castle-web-cli 0.4.169 → 0.4.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-prompts.d.ts +1 -0
- package/dist/agent-prompts.js +3 -0
- package/dist/agent.js +70 -24
- package/dist/atomicFile.d.ts +29 -0
- package/dist/atomicFile.js +120 -0
- package/dist/castle-host/host.js +108 -0
- package/dist/get-deck.js +2 -11
- package/dist/headlessCover.d.ts +4 -0
- package/dist/headlessCover.js +7 -5
- package/dist/ide.d.ts +10 -0
- package/dist/ide.js +19 -11
- package/dist/index.js +10 -6
- package/dist/init.d.ts +1 -1
- package/dist/init.js +11 -1
- package/dist/remix.js +2 -14
- package/dist/save-deck.d.ts +3 -0
- package/dist/save-deck.js +40 -73
- package/dist/saveCover.d.ts +18 -0
- package/dist/saveCover.js +61 -0
- package/dist/serve.js +18 -5
- package/dist/serveSecurity.d.ts +11 -1
- package/dist/serveSecurity.js +38 -4
- package/dist/shell/assets/index-6odVZQSZ.css +1 -0
- package/dist/shell/assets/index-Ws0WrCbi.js +445 -0
- package/dist/shell/index.html +3 -3
- package/dist/versionStore.js +13 -4
- package/kits/base/CLAUDE.md +9 -0
- package/kits/base/castle.json +16 -11
- package/kits/base/sdk/README.md +83 -2
- package/kits/base/sdk/commands.d.ts +37 -1
- package/kits/base/sdk/commands.js +9 -0
- package/kits/base/sdk/lifecycle.d.ts +20 -0
- package/kits/base/sdk/lifecycle.js +7 -1
- package/kits/base/sdk/resumeState.d.ts +2 -0
- package/kits/base/sdk/resumeState.js +84 -0
- package/kits/base/sdk/runtime.js +65 -0
- package/kits/base/sdk/transport.d.ts +3 -0
- package/kits/base/sdk/transport.js +39 -5
- package/kits/base/sdk/unloadHandshake.d.ts +6 -0
- package/kits/base/sdk/unloadHandshake.js +47 -0
- package/kits/base/sdk/user.d.ts +4 -0
- package/kits/base/sdk/user.js +36 -1
- package/kits/multiplayer-2d/CLAUDE.md +20 -9
- package/kits/multiplayer-2d/castle.json +4 -4
- package/kits/multiplayer-2d/code/server/players.js +27 -6
- package/kits/multiplayer-2d/code/server/world.js +24 -1
- package/kits/multiplayer-2d/code/systems/multiplayer.js +45 -4
- package/kits/multiplayer-2d/package-lock.json +2 -4
- package/kits/multiplayer-2d/package.json +1 -0
- package/kits/multiplayer-3d/CLAUDE.md +28 -9
- package/kits/multiplayer-3d/castle.json +5 -5
- package/kits/multiplayer-3d/code/server/players.js +54 -5
- package/kits/multiplayer-3d/code/server/world.js +45 -1
- package/kits/multiplayer-3d/code/systems/multiplayer.js +108 -6
- package/kits/multiplayer-3d/package-lock.json +2 -4
- package/kits/multiplayer-3d/package.json +1 -0
- package/kits/physics-2d/CLAUDE.md +75 -43
- package/kits/physics-2d/behaviors/AnalogStick.jsx +75 -45
- package/kits/physics-2d/behaviors/Slingshot.jsx +1 -2
- package/kits/physics-2d/castle.json +6 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +85 -182
- package/kits/physics-2d/editors/SceneEditor.jsx +25 -4
- package/kits/physics-2d/editors/StyleEditor.jsx +6 -2
- package/kits/physics-2d/editors/overlayCanvas.js +77 -0
- package/kits/physics-2d/editors/pathOverlay.js +5 -13
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +5 -1
- package/kits/physics-2d/editors/pixelInspector.jsx +4 -4
- package/kits/physics-2d/editors/pxArtTimeline.jsx +8 -36
- package/kits/physics-2d/editors/pxArtTimeline.module.css +4 -28
- package/kits/physics-2d/editors/styleEditor.module.css +23 -4
- package/kits/physics-2d/editors/useArtboardZoomPan.js +322 -0
- package/kits/physics-2d/engine/SceneUI.jsx +6 -12
- package/kits/physics-2d/engine/icons.js +56 -0
- package/kits/physics-2d/engine/panelSorting.jsx +7 -14
- package/kits/physics-2d/engine/physics/PhysicsSystem.js +66 -11
- package/kits/physics-2d/engine/physics/controls.js +76 -12
- package/kits/physics-2d/engine/scene.js +47 -9
- package/kits/physics-2d/engine/ui.jsx +13 -125
- package/kits/physics-2d/engine/ui.module.css +53 -28
- package/kits/physics-2d/package-lock.json +0 -29
- package/kits/physics-2d/package.json +3 -1
- package/kits/physics-2d/pnpm-lock.yaml +1 -17
- package/kits/physics-3d/behaviors/Pickup.jsx +5 -3
- package/kits/physics-3d/castle.json +5 -5
- package/kits/physics-3d/package-lock.json +0 -29
- package/kits/physics-3d/package.json +0 -1
- package/kits/physics-3d/pnpm-lock.yaml +0 -17
- package/kits/real-time/CLAUDE.md +23 -4
- package/kits/real-time/castle.json +1 -1
- package/kits/real-time/code/client/connection.js +9 -4
- package/kits/real-time/code/client/joinOverlay.js +46 -0
- package/kits/real-time/code/client/messages.js +4 -0
- package/kits/real-time/code/server/gameHooks.js +4 -0
- package/kits/real-time/code/server/persist.js +105 -0
- package/kits/real-time/code/server/session.js +289 -19
- package/kits/real-time/package-lock.json +1139 -0
- package/kits/turn-based/CLAUDE.md +76 -20
- package/kits/turn-based/castle.json +3 -3
- package/kits/turn-based/code/server/index.js +16 -5
- package/kits/turn-based/package-lock.json +0 -27
- package/kits/turn-based/package.json +2 -2
- package/kits/turn-based/room.js +162 -13
- package/kits/turn-based/testing.js +276 -92
- package/package.json +5 -5
- package/dist/shell/assets/index-CVHj503j.css +0 -1
- package/dist/shell/assets/index-x_QkP3Xq.js +0 -444
|
@@ -22,9 +22,10 @@ Do you already know what you want to make, or do you want to figure it out toget
|
|
|
22
22
|
- Every actor is an instance of a **blueprint** (`blueprints/*.scene`) — see `## Blueprints` below. Always author the blueprint file yourself and reference it via `"blueprint"`. Don't write inline actors (full `components`, no `blueprint` field): the editor auto-migrates each one into its own new blueprint file on open, one per actor with no dedup, which litters the deck with junk blueprints.
|
|
23
23
|
- Real game objects and scenery should usually be editable sprites: generate `.sprite` with `npm run draw -- name`, then place it via a `Sprite` component pointing at `drawings/name.sprite`. Dynamic UI/effects stay procedural.
|
|
24
24
|
- A deck can also hold uploaded media (the Files panel's Upload button): `Sprite` draws image files as well as `.sprite`, and `Video` plays video files. Point their `file` at the deck path (`assets/logo.png`). For audio see `## Sound` below -- sound effects need no component at all.
|
|
25
|
-
- The deck's body/UI font is picked in the Style editor (`theme.style`):
|
|
25
|
+
- The deck's body/UI font is picked in the Style editor (`theme.style`): canvas text reads `scene.font`. A second display face is imported in the behavior that uses it. See `## Deck font` below.
|
|
26
26
|
- This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
|
|
27
27
|
- Space is reserved by the editor for play/stop; do not bind Space to gameplay.
|
|
28
|
+
- Two render hooks: `draw(ctx)` is world space and scrolls with `Camera`; `drawUi(ctx)` is screen space (card units, camera-free). On-screen controls, gauges and HUD go in `drawUi`, never `draw`. The React `ui()` hook is deprecated — prefer `drawUi`.
|
|
28
29
|
- Do not read `engine/`, `editors/`, or built-in behaviors (`Layout.jsx`, `Sprite.jsx`, `Collider.jsx`, `Camera.jsx`, and the physics ones — `RigidBody.jsx`, `Joints.jsx`, `Draggable.jsx`, `Slingshot.jsx`, `AnalogStick.jsx`) to build a game. Their public API is documented below. Forking is the exception: when you're deliberately copying a kit file into the deck to change it — an editor (see `## Editors`) or a behavior you're shadowing — read the original. `engine/` stays off-limits either way.
|
|
29
30
|
- An editor is for content FILES the creator keeps coming back to (catalogs, dialogue, level tables) that nothing already opens. Scenes, sprites and styles have editors; per-actor settings belong on behavior props in the inspector — not in a new editor.
|
|
30
31
|
- Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
|
|
@@ -266,11 +267,16 @@ export class MyThing {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
// Optional. Custom drawing (you usually don't need this; use a Sprite
|
|
269
|
-
// component instead). ctx is
|
|
270
|
+
// component instead). ctx is world space: card units, translated by the
|
|
271
|
+
// camera when a `Camera` exists, then rotated by the actor's Layout.
|
|
270
272
|
draw(actor, scene, ctx) {}
|
|
271
273
|
|
|
272
|
-
// Optional.
|
|
273
|
-
//
|
|
274
|
+
// Optional. Canvas HUD in screen-fixed card units; never moves with the
|
|
275
|
+
// camera. Prefer this for every on-screen control, gauge, and HUD label.
|
|
276
|
+
drawUi(actor, scene, ctx) {}
|
|
277
|
+
|
|
278
|
+
// Deprecated. Prefer `drawUi`. Still runs (React overlay, screen-fixed card
|
|
279
|
+
// units) but new HUD should not use it.
|
|
274
280
|
ui(actor, scene) {
|
|
275
281
|
return null;
|
|
276
282
|
}
|
|
@@ -290,12 +296,6 @@ export class MyThing {
|
|
|
290
296
|
|
|
291
297
|
A fresh `Behavior` instance is constructed per actor per frame from the actor's component props — DO NOT store per-actor state on `this`. Persist transient state on `actor.runtime` (a free-form object the framework will not serialize) or on the component props themselves.
|
|
292
298
|
|
|
293
|
-
Interactive HUD from `ui` works by default for real controls (`button`, `a`,
|
|
294
|
-
`input`, `select`, `textarea`, `[role="button"]`). A tap handler on anything
|
|
295
|
-
else — a plain `div` — is click-through, so the HUD doesn't eat taps meant for
|
|
296
|
-
the game: set `pointerEvents: 'auto'` on that element, never on a wrapper
|
|
297
|
-
covering the card (every behavior's `ui` shares one layer).
|
|
298
|
-
|
|
299
299
|
## Scene file (`scenes/main.scene`, plain JSON)
|
|
300
300
|
|
|
301
301
|
A scene is a background plus a list of actors. Every actor is an **instance of a blueprint** (see `## Blueprints`): a `blueprint` path plus sparse `components` overrides — usually just its `Layout` position:
|
|
@@ -400,7 +400,7 @@ Template `components` keys are behavior names (the `static behaviorName`); add a
|
|
|
400
400
|
}
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
-
- **Camera** — `{ target: actorId, followX, followY, roomWidth, roomHeight }`. Place on a dedicated actor; sets `scene.camera` clamped to the room. Omit entirely for a fixed view (no camera = no translation).
|
|
403
|
+
- **Camera** — `{ target: actorId, followX, followY, roomWidth, roomHeight }`. Place on a dedicated actor; sets `scene.camera` clamped to the room. Omit entirely for a fixed view (no camera = no translation). It translates everything drawn in `draw()`; `drawUi()` is unaffected. `scene.camera` is `{ x, y }` while a Camera actor runs, else `undefined`.
|
|
404
404
|
- **SoundPlayer** — `{ file: "assets/music/theme.mp3", stream?: false, volume?: 1, playbackRate?: 1, pan?: 0, loop?: false, playOnStart?: true }`. Plays one audio file from an actor. `stream: false` (the default) fires it from memory — instant and overlapping, for effects; `stream: true` streams it from an `<audio>` element — for music and long ambience, whose decoded audio would be far too big to hold. `loop` applies to `stream: true` only. `playbackRate` retunes it (2 = an octave up and twice as fast, 0.5 = an octave down); `pan` places it left (−1) to right (+1). With `playOnStart` it starts with the scene; otherwise trigger it through the handle it puts on the actor:
|
|
405
405
|
|
|
406
406
|
```jsx
|
|
@@ -442,7 +442,7 @@ Name a sound by its filename without the extension, or by its full deck path. Tw
|
|
|
442
442
|
|
|
443
443
|
A physics-2d deck picks its **deck font** in the Style editor — open
|
|
444
444
|
`theme.style` and choose one of the nine Castle faces, or None. That is the
|
|
445
|
-
deck's body and UI face:
|
|
445
|
+
deck's body and UI face: `scene.font` returns it for `draw` / `drawUi`.
|
|
446
446
|
|
|
447
447
|
Picking writes two files, and both belong in the deck:
|
|
448
448
|
|
|
@@ -451,25 +451,19 @@ Picking writes two files, and both belong in the deck:
|
|
|
451
451
|
- `fonts.generated.js` — one static re-export, and the only thing that carries
|
|
452
452
|
that face's bytes into the published deck. Generated; don't edit it.
|
|
453
453
|
|
|
454
|
-
Use it in
|
|
455
|
-
|
|
456
|
-
- **HUD text** (a behavior's `ui` hook) inherits it already — plain React, no
|
|
457
|
-
extra work. `--castle-deck-font` is on the UI layer if you want to name it in
|
|
458
|
-
CSS.
|
|
459
|
-
- **In-world text** (a behavior's `draw` hook) reads `scene.font`, which is
|
|
460
|
-
always a usable CSS family — the deck's face, or a sans stack when it has
|
|
461
|
-
none:
|
|
454
|
+
Use it from canvas text in `draw` / `drawUi` — both read `scene.font`, which is
|
|
455
|
+
always a usable CSS family (the deck's face, or a sans stack when it has none):
|
|
462
456
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
457
|
+
```jsx
|
|
458
|
+
drawUi(actor, scene, ctx) {
|
|
459
|
+
ctx.font = `24px ${scene.font}`;
|
|
460
|
+
ctx.fillText(`${this.props.score}`, 20, 40);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
469
463
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
464
|
+
Don't hardcode the deck font's family name there: a hook naming `'Piazzolla'`
|
|
465
|
+
outright keeps drawing in it after someone picks a different font, and draws in
|
|
466
|
+
nothing at all once they pick None.
|
|
473
467
|
|
|
474
468
|
### A second face
|
|
475
469
|
|
|
@@ -498,8 +492,8 @@ behavior imported for itself — before the first frame, so `ctx.font` is safe t
|
|
|
498
492
|
set from the very first `draw`. Each face costs its own 12–90 KB in the bundle,
|
|
499
493
|
so two or three in total is the shape to aim for.
|
|
500
494
|
|
|
501
|
-
There is no Text behavior — text is drawn by the deck's own code
|
|
502
|
-
|
|
495
|
+
There is no Text behavior — text is drawn by the deck's own code via `draw`
|
|
496
|
+
(world) or `drawUi` (screen HUD).
|
|
503
497
|
|
|
504
498
|
## Physics
|
|
505
499
|
|
|
@@ -544,13 +538,21 @@ tilts it sideways. Omit for the default (`1`, downward).
|
|
|
544
538
|
|
|
545
539
|
### Driving physics from behaviors — `scene.physics`
|
|
546
540
|
|
|
547
|
-
In a behavior's `update`, steer bodies through `scene.physics
|
|
548
|
-
`Layout`
|
|
541
|
+
In a behavior's `update`, steer bodies through `scene.physics`. Writing
|
|
542
|
+
`Layout.x`/`y`/`rotation` on a dynamic body **teleports** it there before the
|
|
543
|
+
next step (velocity kept, no collision along the way). To move it *under*
|
|
544
|
+
physics use `setVelocity`/`applyImpulse`/`applyForce`:
|
|
549
545
|
|
|
550
546
|
- `scene.physics.setVelocity(actor, { x, y })` — set velocity (px per fixed
|
|
551
547
|
step; multiply by ~60 for px/s).
|
|
552
548
|
- `scene.physics.applyImpulse(actor, { x, y })` — add to velocity this frame.
|
|
553
549
|
- `scene.physics.applyForce(actor, { x, y })` — add a matter-space force.
|
|
550
|
+
- `scene.physics.setRotation(actor, deg)` — same as writing `Layout.rotation`.
|
|
551
|
+
- `scene.physics.setPosition(actor, { x, y })` — same as writing Layout; x/y
|
|
552
|
+
are the Layout top-left.
|
|
553
|
+
- `scene.physics.setAngularVelocity(actor, degPerStep)` /
|
|
554
|
+
`getAngularVelocity(actor)` — set to 0 to stop a spin;
|
|
555
|
+
`setVelocity(actor, {x:0,y:0})` stops linear motion.
|
|
554
556
|
- `scene.physics.getVelocity(actor)` — current velocity `{ x, y }`.
|
|
555
557
|
- `scene.physics.isColliding(a, b)` — are these two actors touching right now?
|
|
556
558
|
- `scene.physics.actorAtPoint(x, y)` — topmost actor whose **collider** contains
|
|
@@ -578,6 +580,26 @@ export class Coin {
|
|
|
578
580
|
}
|
|
579
581
|
```
|
|
580
582
|
|
|
583
|
+
### Stick hooks
|
|
584
|
+
|
|
585
|
+
An `AnalogStick` on the same actor fires sibling hooks when the gesture begins
|
|
586
|
+
and ends — begin on the first nonzero deflection (not on a still press), end on
|
|
587
|
+
release or when the stick is disabled mid-hold. Signature `(actor, scene)`; the
|
|
588
|
+
stick itself receives them and ignores them.
|
|
589
|
+
|
|
590
|
+
```jsx
|
|
591
|
+
export class StickSlowDownToggle {
|
|
592
|
+
static behaviorName = 'StickSlowDownToggle';
|
|
593
|
+
onAnalogStickBegin(actor, scene) {
|
|
594
|
+
// e.g. mute a sibling Slow Down while the stick is active
|
|
595
|
+
actor.runtime.stickHeld = true;
|
|
596
|
+
}
|
|
597
|
+
onAnalogStickEnd(actor, scene) {
|
|
598
|
+
actor.runtime.stickHeld = false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
581
603
|
### Built-in controls (add these behaviors, no code needed)
|
|
582
604
|
|
|
583
605
|
These read input and drive the actor through the physics system, so the actor
|
|
@@ -616,7 +638,13 @@ see the SceneRuntime API below.
|
|
|
616
638
|
- **`Slingshot`** — press the actor, pull back, release to launch it the
|
|
617
639
|
opposite way (angry-birds), with an aim arrow. `speed`, `maxDrag`.
|
|
618
640
|
- **`AnalogStick`** — on-screen virtual joystick (press anywhere) that drives
|
|
619
|
-
the actor
|
|
641
|
+
the actor with force-mode feel: ramp in, hard top speed, idle-only brake.
|
|
642
|
+
Screen-fixed (`drawUi`) so it works with a `Camera`. `speed` (top speed,
|
|
643
|
+
px/step), `rampTime` (seconds rest→speed at full deflection; `0` = instant),
|
|
644
|
+
`slowDown` (idle deceleration in speed units per second; `0` = coast),
|
|
645
|
+
`turnBoost` (extra accel when reversing), `maxRadius`, `axes`. Never clamps
|
|
646
|
+
or brakes an undriven axis (so horizontal + gravity still falls). Best for
|
|
647
|
+
top-down; see **Stick hooks** above for begin/end.
|
|
620
648
|
|
|
621
649
|
### Recipe: a launch-and-bounce game
|
|
622
650
|
|
|
@@ -712,10 +740,13 @@ draw(actor, scene, ctx) {
|
|
|
712
740
|
Slingshot) can pass straight through it — matter-js has no continuous
|
|
713
741
|
collision. Keep boundary walls thick (50px+) and/or cap launch power. The
|
|
714
742
|
demo scene uses 50px walls with a ~20px/step launch cap.
|
|
715
|
-
- **
|
|
716
|
-
|
|
717
|
-
`
|
|
718
|
-
|
|
743
|
+
- **Writing `Layout` teleports.** A Layout write on a body snaps it there
|
|
744
|
+
before the next step with no collision response on the way — a per-frame
|
|
745
|
+
`layout.x += speed*dt` on a dynamic body ghosts through walls and jitters
|
|
746
|
+
(use `setVelocity` for that). Momentum is kept, so to drive rotation every
|
|
747
|
+
frame pair it with `freezeRotation` or expect a small spin drift between
|
|
748
|
+
writes. Teleporting one end of a joint makes the constraint snap it back
|
|
749
|
+
next step.
|
|
719
750
|
- **Editor vs play.** Physics only steps during play; in the editor actors sit
|
|
720
751
|
where you place them. `RigidBody.velocityX/Y` apply once when play starts.
|
|
721
752
|
- **No `Collider` means no physics at all.** A body only exists for an actor with
|
|
@@ -791,8 +822,9 @@ To generate path art, emit an svg of **shapes** rather than per-pixel rects and
|
|
|
791
822
|
|
|
792
823
|
- `scene.time` — seconds since start.
|
|
793
824
|
- `scene.keys` — `Set` of currently-held KeyboardEvent codes (e.g. `'ArrowLeft'`, `'KeyA'`, `'Space'`). Read in `update`.
|
|
794
|
-
- `scene.pointer` — `{ x, y, down }`
|
|
795
|
-
- `scene.pointers` — `Map` of every active pointer (finger / mouse / stylus) by id, each `{ id, x, y, down, justPressed, claimedBy }`, same coordinates. For multi-touch: latch the `id` a gesture began on and follow that entry, so another finger landing can't hijack it. `justPressed` is that pointer's press edge for this frame (a shared `down` flag would miss a finger landing while another is already held). Entries disappear on release.
|
|
825
|
+
- `scene.pointer` — `{ x, y, screenX, screenY, down }`. `x`/`y` are world coordinates (card units offset by the camera); `screenX`/`screenY` are card coords before the camera, for screen-fixed controls. The **primary** pointer: the one held longest. Use this for anything single-touch.
|
|
826
|
+
- `scene.pointers` — `Map` of every active pointer (finger / mouse / stylus) by id, each `{ id, x, y, screenX, screenY, down, justPressed, claimedBy }`, same coordinates. For multi-touch: latch the `id` a gesture began on and follow that entry, so another finger landing can't hijack it. `justPressed` is that pointer's press edge for this frame (a shared `down` flag would miss a finger landing while another is already held). Entries disappear on release.
|
|
827
|
+
- `scene.camera` — `{ x, y }` while a Camera actor runs, else `undefined`.
|
|
796
828
|
- `scene.claimPointer(id, ownerId, priority?)` / `scene.ownsPointer(id, ownerId)` — take a finger for one gesture so two controls can't both act on it, and check you still hold it. A higher `priority` takes it from a lower one, which is how a control that was actually pressed (a `Draggable`'s collider) beats one that takes any press (an `AnalogStick`), regardless of actor order. Claims vanish with the pointer, so there's nothing to release. This is what lets a player work a stick with one thumb and drag with the other.
|
|
797
829
|
- `scene.getActor(id)` / `scene.getActors()` (sorted by Layout.z) / `scene.getComponent(actor, name)`.
|
|
798
830
|
- `scene.actorWith('GameController')` / `scene.actorsWith('Brick')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
|
|
@@ -803,7 +835,7 @@ To generate path art, emit an svg of **shapes** rather than per-pixel rects and
|
|
|
803
835
|
- `scene.spawnFromBlueprint('blueprints/enemy.scene', { components: { Layout: { x, y } } })` — spawn an instance of a blueprint, same merge semantics as a placed instance. Prefer this over `spawnActor` when you're spawning copies of something that has (or should have) a blueprint, e.g. `scene.spawnFromBlueprint(actor.blueprint, { components: { Layout: { x: actor.components.Layout.x, y: actor.components.Layout.y } } })` to spawn another of the same kind as an existing actor.
|
|
804
836
|
- `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
|
|
805
837
|
- `scene.sound` — the deck's sound library: `play(name, opts)`, `stopAll()`, `isLoaded`, `whenLoaded`, `has(name)`, `voiceCount`. See `## Sound`.
|
|
806
|
-
- `scene.font` — the deck's font as a CSS family, for `ctx.font` in `draw`. Always usable: the face picked in the Style editor, or a sans stack. See `## Deck font`.
|
|
838
|
+
- `scene.font` — the deck's font as a CSS family, for `ctx.font` in `draw` / `drawUi`. Always usable: the face picked in the Style editor, or a sans stack. See `## Deck font`.
|
|
807
839
|
- `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
|
|
808
840
|
- `scene.load(sceneData)` — replace the running scene with the given scene data object.
|
|
809
841
|
- `scene.readFromFile(name)` — read and parse a scene file (`'gameover.scene'`, `'levels/2.scene'`), returning its scene data. Use this instead of importing a `.scene` file.
|
|
@@ -820,14 +852,14 @@ if (scene.keys.has('KeyX')) /* launch ball */ ;
|
|
|
820
852
|
|
|
821
853
|
**Space is reserved** — the editor binds it to the play/stop toggle, so don't bind Space to a gameplay action (jump / shoot / launch / ...). Use arrows, WASD, letter keys, or on-screen buttons instead.
|
|
822
854
|
|
|
823
|
-
For HUD text
|
|
855
|
+
For HUD text and shapes use `drawUi` (screen-fixed canvas). Only in-world text or shapes go in `draw`. Both read `scene.font` — see `## Deck font`. The React `ui()` hook is deprecated.
|
|
824
856
|
|
|
825
857
|
## Common breakout-shaped recipe (sketch)
|
|
826
858
|
|
|
827
859
|
- `Paddle` behavior: read keys, clamp x to `[0, 500 - layout.width]`.
|
|
828
860
|
- `Ball` behavior: store `vx, vy` on `actor.runtime`; integrate; bounce off wall edges (`x<0`, `x+w>500`, `y<0`); on `scene.overlaps(actor, paddle)`, flip `vy`; for each `brick` in `scene.actorsWith('Brick')` check `scene.overlaps(actor, brick)` → flip `vy` and `scene.despawnActor(brick.id)`; if `y > 700` lose a life.
|
|
829
861
|
- `Brick` behavior: typically just a marker — `kind: 'solid'` collider is enough. State (hit count) goes on `actor.runtime` or the brick's own props.
|
|
830
|
-
- `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `
|
|
862
|
+
- `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `drawUi()`.
|
|
831
863
|
|
|
832
864
|
## Don't
|
|
833
865
|
|
|
@@ -4,19 +4,39 @@ import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
|
4
4
|
import {
|
|
5
5
|
GREEDY_CLAIM,
|
|
6
6
|
acquirePress,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
length,
|
|
8
|
+
screenPoint,
|
|
9
|
+
stickBrake,
|
|
10
|
+
stickDrive,
|
|
9
11
|
stickVector,
|
|
10
12
|
} from '../engine/physics/controls';
|
|
11
13
|
|
|
12
|
-
// AnalogStick:
|
|
13
|
-
//
|
|
14
|
-
// the
|
|
15
|
-
//
|
|
14
|
+
// AnalogStick: on-screen virtual joystick (press anywhere) that drives the
|
|
15
|
+
// actor with OG-style force mode — ramp in, hard top speed, idle-only brake.
|
|
16
|
+
// Screen-fixed: the ring stays under the thumb while a Camera scrolls, and does
|
|
17
|
+
// not rotate with the actor. Needs a RigidBody (dynamic) + Collider. Never
|
|
18
|
+
// touches an undriven axis.
|
|
16
19
|
//
|
|
17
|
-
// speed ->
|
|
20
|
+
// speed -> top speed at full deflection (px per fixed step).
|
|
21
|
+
// rampTime -> seconds from rest to speed at full deflection (0 = instant).
|
|
22
|
+
// slowDown -> deceleration while idle, speed units lost per second (0 = coast).
|
|
23
|
+
// turnBoost -> extra accel when velocity points away from the stick (OG turnFriction).
|
|
18
24
|
// maxRadius -> stick throw radius in pixels.
|
|
19
|
-
// axes ->
|
|
25
|
+
// axes -> 'horizontal' / 'vertical' / 'both'.
|
|
26
|
+
//
|
|
27
|
+
// Sibling hooks (fired on this actor only): onAnalogStickBegin / onAnalogStickEnd
|
|
28
|
+
// (actor, scene) — begin on first nonzero deflection, end on release or disable.
|
|
29
|
+
function fireStickHook(actor, scene, hook) {
|
|
30
|
+
scene.forEachBehavior(actor, (inst) => inst[hook]?.(actor, scene));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearStickRuntime(rt) {
|
|
34
|
+
rt._stickBegan = false;
|
|
35
|
+
rt._stickPointerId = null;
|
|
36
|
+
rt._stickOrigin = null;
|
|
37
|
+
rt._stickKnob = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
export class AnalogStick {
|
|
21
41
|
static behaviorName = 'AnalogStick';
|
|
22
42
|
// Drives a body, so it needs one: Collider + dynamic RigidBody.
|
|
@@ -24,14 +44,20 @@ export class AnalogStick {
|
|
|
24
44
|
|
|
25
45
|
static defaultProps = {
|
|
26
46
|
speed: 6,
|
|
47
|
+
rampTime: 0.2,
|
|
48
|
+
slowDown: 20,
|
|
49
|
+
turnBoost: 3,
|
|
27
50
|
maxRadius: 60,
|
|
28
51
|
axes: 'both',
|
|
29
52
|
};
|
|
30
53
|
|
|
31
54
|
// `maxRadius` is the stick's travel in px; below ~10 the knob has no usable
|
|
32
|
-
// throw and the control reads as broken.
|
|
55
|
+
// throw and the control reads as broken. slowDown humanizes to "Slow Down".
|
|
33
56
|
static propertyMeta = {
|
|
34
57
|
speed: { min: 0, step: 0.5 },
|
|
58
|
+
rampTime: { min: 0, step: 0.05 },
|
|
59
|
+
slowDown: { min: 0, step: 1 },
|
|
60
|
+
turnBoost: { min: 0, step: 0.5 },
|
|
35
61
|
maxRadius: { min: 10, step: 5 },
|
|
36
62
|
};
|
|
37
63
|
|
|
@@ -55,59 +81,63 @@ export class AnalogStick {
|
|
|
55
81
|
const press = acquirePress(scene, actor.id, () => GREEDY_CLAIM);
|
|
56
82
|
if (!press) return null;
|
|
57
83
|
rt._stickPointerId = press.id;
|
|
58
|
-
rt._stickOrigin = { x: press.
|
|
84
|
+
rt._stickOrigin = { x: press.screenX, y: press.screenY };
|
|
59
85
|
return press;
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
update(actor, scene) {
|
|
88
|
+
update(actor, scene, dt) {
|
|
63
89
|
const rt = actor.runtime;
|
|
64
90
|
const p = this.trackedPointer(actor, scene);
|
|
65
|
-
// Only drive velocity while the stick is actually held.
|
|
66
91
|
if (p?.down && rt._stickOrigin) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
rt.
|
|
92
|
+
const maxRadius = this.props.maxRadius ?? 60;
|
|
93
|
+
const sp = screenPoint(p);
|
|
94
|
+
const knob = stickVector(sp, rt._stickOrigin, maxRadius);
|
|
95
|
+
rt._stickKnob = knob;
|
|
96
|
+
const deflection = { x: knob.x / maxRadius, y: knob.y / maxRadius };
|
|
97
|
+
const vel = scene.physics.getVelocity(actor);
|
|
98
|
+
scene.physics.applyImpulse(actor, stickDrive(vel, deflection, this.props, dt));
|
|
99
|
+
// OG fires on first movement, not on a still press.
|
|
100
|
+
if (!rt._stickBegan && length(deflection) > 0) {
|
|
101
|
+
rt._stickBegan = true;
|
|
102
|
+
fireStickHook(actor, scene, 'onAnalogStickBegin');
|
|
103
|
+
}
|
|
70
104
|
return;
|
|
71
105
|
}
|
|
72
106
|
|
|
73
|
-
rt.
|
|
74
|
-
|
|
75
|
-
rt._stickKnob = null;
|
|
76
|
-
// Stop ONCE on release, and only on the driven axes (so a top-down mover
|
|
77
|
-
// doesn't drift). Critically, do NOT touch velocity while idle -- doing that
|
|
78
|
-
// every frame zeroes the velocity gravity builds up, so the actor barely
|
|
79
|
-
// falls. An idle AnalogStick must leave physics (gravity, momentum) alone.
|
|
80
|
-
if (rt._stickActive) {
|
|
81
|
-
rt._stickActive = false;
|
|
82
|
-
const axes = this.props.axes ?? 'both';
|
|
83
|
-
const cur = scene.physics.getVelocity(actor);
|
|
84
|
-
scene.physics.setVelocity(actor, {
|
|
85
|
-
x: axes === 'vertical' ? cur.x : 0,
|
|
86
|
-
y: axes === 'horizontal' ? cur.y : 0,
|
|
87
|
-
});
|
|
107
|
+
if (rt._stickBegan) {
|
|
108
|
+
fireStickHook(actor, scene, 'onAnalogStickEnd');
|
|
88
109
|
}
|
|
110
|
+
clearStickRuntime(rt);
|
|
111
|
+
// Idle every frame: brake on driven axes only (gravity/momentum elsewhere stay).
|
|
112
|
+
const vel = scene.physics.getVelocity(actor);
|
|
113
|
+
scene.physics.applyImpulse(actor, stickBrake(vel, this.props, dt));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Switched off mid-hold: fire end and drop runtime state. `update` stops
|
|
117
|
+
// running once disabled, so this is the only chance to release the gesture.
|
|
118
|
+
onDisable(actor, scene) {
|
|
119
|
+
const rt = actor.runtime;
|
|
120
|
+
if (rt._stickBegan) fireStickHook(actor, scene, 'onAnalogStickEnd');
|
|
121
|
+
clearStickRuntime(rt);
|
|
89
122
|
}
|
|
90
123
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
draw(actor, scene, ctx) {
|
|
124
|
+
// Drawn in screen space (drawUi) so the ring stays under the thumb while the
|
|
125
|
+
// camera scrolls, and doesn't rotate with the actor.
|
|
126
|
+
drawUi(actor, scene, ctx) {
|
|
95
127
|
const rt = actor.runtime;
|
|
96
128
|
if (!rt._stickOrigin) return;
|
|
97
129
|
const o = rt._stickOrigin;
|
|
98
130
|
const k = rt._stickKnob ?? { x: 0, y: 0 };
|
|
99
131
|
const radius = this.props.maxRadius ?? 60;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
c.fill();
|
|
110
|
-
});
|
|
132
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
|
133
|
+
ctx.lineWidth = 3;
|
|
134
|
+
ctx.beginPath();
|
|
135
|
+
ctx.arc(o.x, o.y, radius, 0, Math.PI * 2);
|
|
136
|
+
ctx.stroke();
|
|
137
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
138
|
+
ctx.beginPath();
|
|
139
|
+
ctx.arc(o.x + k.x, o.y + k.y, 20, 0, Math.PI * 2);
|
|
140
|
+
ctx.fill();
|
|
111
141
|
}
|
|
112
142
|
|
|
113
143
|
static Inspector({ component, setComponent, override }) {
|
|
@@ -100,8 +100,7 @@ export class Slingshot {
|
|
|
100
100
|
const dir = { x: -pull.x / len, y: -pull.y / len }; // launch direction (opposite pull)
|
|
101
101
|
const end = { x: a.x - pull.x * 0.8, y: a.y - pull.y * 0.8 };
|
|
102
102
|
|
|
103
|
-
//
|
|
104
|
-
// draws the sling as a screen overlay for the same reason).
|
|
103
|
+
// Aim is drawn in world space because the anchor is on the actor.
|
|
105
104
|
inActorWorldSpace(ctx, actor.components.Layout, (c) => drawSling(c, a, end, dir));
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"ext": ".sprite",
|
|
41
41
|
"label": "Sprite",
|
|
42
42
|
"new": "New sprite",
|
|
43
|
-
"icon": "
|
|
43
|
+
"icon": "images-square",
|
|
44
44
|
"editor": "kit",
|
|
45
45
|
"data": true
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"ext": ".pxart",
|
|
49
49
|
"label": "Sprite",
|
|
50
|
-
"icon": "
|
|
50
|
+
"icon": "images-square",
|
|
51
51
|
"editor": "kit",
|
|
52
52
|
"data": true
|
|
53
53
|
},
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"ext": ".jsx",
|
|
56
56
|
"label": "Behavior",
|
|
57
57
|
"new": "New behavior",
|
|
58
|
-
"icon": "code"
|
|
58
|
+
"icon": "file-code"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"ext": ".style",
|
|
62
62
|
"label": "Style",
|
|
63
|
-
"icon": "
|
|
63
|
+
"icon": "palette",
|
|
64
64
|
"editor": "kit",
|
|
65
65
|
"data": true
|
|
66
66
|
}
|
|
@@ -72,11 +72,11 @@
|
|
|
72
72
|
"main": "main.jsx",
|
|
73
73
|
"autoUpdateWhenImported": true,
|
|
74
74
|
"title": "physics-2d",
|
|
75
|
-
"publishedVersion": "2026-09-
|
|
75
|
+
"publishedVersion": "2026-09-10T20:09:53.271Z",
|
|
76
76
|
"imports": {
|
|
77
77
|
"castle.base": {
|
|
78
78
|
"deckId": "yRcmH4_aYllE",
|
|
79
|
-
"version": "2026-09-
|
|
79
|
+
"version": "2026-09-10T00:56:59.388Z"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|