castle-web-cli 0.4.84 → 0.4.85

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.
Files changed (73) hide show
  1. package/dist/ide.js +35 -13
  2. package/dist/shell/assets/{index-BOgm5T3W.js → index-BJLaUTJE.js} +21 -21
  3. package/dist/shell/index.html +1 -1
  4. package/kits/physics-2d/.prettierrc +8 -0
  5. package/kits/physics-2d/CLAUDE.md +329 -0
  6. package/kits/physics-2d/behaviors/Camera.jsx +43 -0
  7. package/kits/physics-2d/behaviors/Collider.jsx +199 -0
  8. package/kits/physics-2d/behaviors/Goal.jsx +29 -0
  9. package/kits/physics-2d/behaviors/Layout.jsx +53 -0
  10. package/kits/physics-2d/behaviors/Sprite.jsx +352 -0
  11. package/kits/physics-2d/behaviors/tint.js +47 -0
  12. package/kits/physics-2d/blueprints/ball.scene +14 -0
  13. package/kits/physics-2d/blueprints/block.scene +12 -0
  14. package/kits/physics-2d/blueprints/cauldron.scene +18 -0
  15. package/kits/physics-2d/blueprints/crate.scene +14 -0
  16. package/kits/physics-2d/blueprints/goal.scene +12 -0
  17. package/kits/physics-2d/castle.json +13 -0
  18. package/kits/physics-2d/docs/pxart-format.md +377 -0
  19. package/kits/physics-2d/drawings/block.pxart +25 -0
  20. package/kits/physics-2d/drawings/cauldron.pxart +113 -0
  21. package/kits/physics-2d/editors/BlueprintLibrary.jsx +247 -0
  22. package/kits/physics-2d/editors/ErrorBoundary.jsx +59 -0
  23. package/kits/physics-2d/editors/PlayOnly.jsx +31 -0
  24. package/kits/physics-2d/editors/PxArtEditor.jsx +954 -0
  25. package/kits/physics-2d/editors/SceneEditor.jsx +1681 -0
  26. package/kits/physics-2d/editors/SelectionOverlay.jsx +909 -0
  27. package/kits/physics-2d/editors/SingleEditor.jsx +122 -0
  28. package/kits/physics-2d/editors/behaviorRegistry.js +30 -0
  29. package/kits/physics-2d/editors/editorHistory.js +157 -0
  30. package/kits/physics-2d/editors/inspectorSheet.js +13 -0
  31. package/kits/physics-2d/editors/pixelCanvas.js +11 -0
  32. package/kits/physics-2d/editors/pixelEditorChrome.jsx +74 -0
  33. package/kits/physics-2d/editors/pixelGeometry.js +140 -0
  34. package/kits/physics-2d/editors/pixelInspector.jsx +633 -0
  35. package/kits/physics-2d/editors/pxArtEditorModel.js +732 -0
  36. package/kits/physics-2d/editors/pxArtPlayback.js +92 -0
  37. package/kits/physics-2d/editors/pxArtTimeline.jsx +752 -0
  38. package/kits/physics-2d/editors/pxArtTimeline.module.css +506 -0
  39. package/kits/physics-2d/editors/pxArtTools.js +232 -0
  40. package/kits/physics-2d/editors/useArtboardFit.js +102 -0
  41. package/kits/physics-2d/engine/ScenePlayer.jsx +196 -0
  42. package/kits/physics-2d/engine/SceneUI.jsx +59 -0
  43. package/kits/physics-2d/engine/assets.js +15 -0
  44. package/kits/physics-2d/engine/autoInspector.jsx +70 -0
  45. package/kits/physics-2d/engine/blueprint.js +521 -0
  46. package/kits/physics-2d/engine/collider.js +196 -0
  47. package/kits/physics-2d/engine/files.js +117 -0
  48. package/kits/physics-2d/engine/liveReload.js +88 -0
  49. package/kits/physics-2d/engine/pxart.js +1032 -0
  50. package/kits/physics-2d/engine/pxartSmooth.js +222 -0
  51. package/kits/physics-2d/engine/scene.js +686 -0
  52. package/kits/physics-2d/engine/spriteGeometry.js +32 -0
  53. package/kits/physics-2d/engine/ui.jsx +688 -0
  54. package/kits/physics-2d/engine/ui.module.css +2287 -0
  55. package/kits/physics-2d/eslint.config.js +71 -0
  56. package/kits/physics-2d/index.html +24 -0
  57. package/kits/physics-2d/main.jsx +24 -0
  58. package/kits/physics-2d/package-lock.json +2706 -0
  59. package/kits/physics-2d/package.json +42 -0
  60. package/kits/physics-2d/physics/PhysicsSystem.js +290 -0
  61. package/kits/physics-2d/physics/behaviors/AnalogStick.jsx +101 -0
  62. package/kits/physics-2d/physics/behaviors/Draggable.jsx +79 -0
  63. package/kits/physics-2d/physics/behaviors/RigidBody.jsx +55 -0
  64. package/kits/physics-2d/physics/behaviors/Slingshot.jsx +118 -0
  65. package/kits/physics-2d/physics/controls.js +79 -0
  66. package/kits/physics-2d/physics/index.js +26 -0
  67. package/kits/physics-2d/physics/matterBridge.js +126 -0
  68. package/kits/physics-2d/pnpm-lock.yaml +1761 -0
  69. package/kits/physics-2d/scenes/main.scene +12 -0
  70. package/kits/physics-2d/scenes/sandbox.scene +13 -0
  71. package/kits/physics-2d/scripts/draw.mjs +121 -0
  72. package/kits/physics-2d/vite.config.js +1 -0
  73. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Castle Editor</title>
7
- <script type="module" crossorigin src="/__castle/ide/assets/index-BOgm5T3W.js"></script>
7
+ <script type="module" crossorigin src="/__castle/ide/assets/index-BJLaUTJE.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-DonnH--m.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "bracketSameLine": true,
6
+ "trailingComma": "es5",
7
+ "arrowParens": "always"
8
+ }
@@ -0,0 +1,329 @@
1
+ # physics-2d kit
2
+
3
+ This is the `basic-2d` actor/behavior/scene framework plus a built-in 2D
4
+ **physics** system (matter-js). Everything in `basic-2d` works the same; physics
5
+ adds `RigidBody`, physics fields on `Collider`, a world-gravity scene setting,
6
+ collision callbacks, and ready-made touch-first controls (`Draggable`,
7
+ `Slingshot`, `AnalogStick`). See `## Physics` below.
8
+
9
+ ## Welcome message
10
+
11
+ Welcome to an early test of Castle's new engine, with real 2D physics! You're starting with Castle's art and scene editors. Open `cauldron.pxart` or `main.scene` to try them out, or I can start building something for you — a physics toy, a launcher game, a drag-and-fling puzzle, whatever you like.
12
+
13
+ Do you already know what you want to make, or do you want to figure it out together?
14
+
15
+ ## Quick reference
16
+
17
+ <!-- Injected into Castle's create assistant every turn; keep this compact and factual. -->
18
+
19
+ - Actor / behavior / scene framework on a fixed 500x700 canvas ("card").
20
+ - Game logic lives in `behaviors/*.jsx` classes. A behavior's `static behaviorName` must match the component key used in scene JSON.
21
+ - Editable screens live in `scenes/*.scene` files (plain JSON). Use a separate scene file per distinct screen, and switch with `scene.loadFromFile('name.scene')`.
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
+ - Real game objects and scenery should usually be editable sprites: generate `.pxart` with `npm run draw -- name`, then place it via a `Sprite` component pointing at `drawings/name.pxart`. Dynamic UI/effects stay procedural.
24
+ - This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
25
+ - After any code, scene, or drawing edit, run `npm run restart`.
26
+ - Space is reserved by the editor for play/stop; do not bind Space to gameplay.
27
+ - Do not read `engine/`, `editors/`, or built-in behaviors (`Layout.jsx`, `Sprite.jsx`, `Collider.jsx`, `Camera.jsx`) to build a game. Their public API is documented below.
28
+ - Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
29
+
30
+ ## Scope
31
+
32
+ Write the smallest game that satisfies what the user asked for. No sound, particles, menus, multi-level progression, or visual polish unless they specifically asked for it. A typical behavior is 30–80 lines — if yours is hitting 200, you're over-engineering: cut feel-good extras, fewer fields on props, fewer edge cases, fewer comments. Ship the core loop first; the user can ask for more.
33
+
34
+ ## Workflow
35
+
36
+ The deck is already serving when you start (`castle-web init` set that up; see `.castle/serve.json` for the URL). The user is watching that page right now. Your job is to make it interesting incrementally:
37
+
38
+ 1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change), `npm run restart`, then add the next piece. Do NOT write the whole game in one shot.
39
+ 2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
40
+ 3. **Prefer real, editable assets.** For game objects, characters, and scenery, make actual pixel-art sprites and place them as real actors in `scenes/*.scene` — not shapes drawn in code. Real assets let the creator move and re-skin things in the editor and let other creators remix the deck. Make art as `.pxart` via the `draw` command (see **Creating pixel art** below). Data-driven UI (health bars, score/text, HUD gauges) and dynamic things (bullets, particles, effects) are correctly procedural/code — don't force those into sprites.
41
+ 4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.loadFromFile('gameover.scene')` (reads the file and transitions) on play / win / level change. NEVER `import` a `.scene` file as a module — scene files are data, not modules; use `scene.readFromFile` / `scene.loadFromFile`.
42
+
43
+ Card size is **500 wide × 700 tall** (origin top-left, +y is down).
44
+
45
+ ## Behavior shape
46
+
47
+ A behavior is a class. Minimal contract:
48
+
49
+ ```jsx
50
+ // behaviors/MyThing.jsx
51
+ export class MyThing {
52
+ static behaviorName = 'MyThing'; // must match the key used in .scene
53
+ static defaultProps = { speed: 200 };
54
+
55
+ constructor(props) {
56
+ this.props = props;
57
+ }
58
+
59
+ // Called every frame in play mode. dt is seconds.
60
+ update(actor, scene, dt) {
61
+ const layout = actor.components.Layout;
62
+ layout.x += this.props.speed * dt; // mutate component in place
63
+ }
64
+
65
+ // Optional. Custom drawing (you usually don't need this; use a Sprite
66
+ // component instead). ctx is in card units already.
67
+ draw(actor, scene, ctx) {}
68
+
69
+ // Optional. Return React nodes for game-time HUD. Coordinates are card
70
+ // units. Read state your `update` set; do not start your own loops.
71
+ ui(actor, scene) {
72
+ return null;
73
+ }
74
+ }
75
+ ```
76
+
77
+ 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.
78
+
79
+ ## Scene file (`scenes/main.scene`, plain JSON)
80
+
81
+ 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:
82
+
83
+ ```json
84
+ {
85
+ "background": "#1b2030",
86
+ "actors": [
87
+ { "id": "paddle", "blueprint": "blueprints/paddle.scene", "components": { "Layout": { "x": 200, "y": 650 } } },
88
+ { "id": "brick-1", "blueprint": "blueprints/brick.scene", "components": { "Layout": { "x": 50, "y": 75 } } },
89
+ { "id": "brick-2", "blueprint": "blueprints/brick.scene", "components": { "Layout": { "x": 100, "y": 75 } } }
90
+ ]
91
+ }
92
+ ```
93
+
94
+ Rules: every actor needs a unique `id` (any string). An instance's `components` holds ONLY props that differ from its blueprint (merged per-property on top of the template); anything unspecified falls back to the blueprint, then to the behavior's `defaultProps`. Position (`Layout.x`/`y`/`rotation`) always lives on the instance. Omit optional fields (`z`, `rotation`, actor `name`, `tint: '#ffffffff'`, scene `name`) to keep scenes compact, especially when generating many actors.
95
+
96
+ ## Blueprints
97
+
98
+ A **blueprint** is a `.scene` file under `blueprints/` whose single actor (`actors[0]`, no `id`) is the template:
99
+
100
+ ```json
101
+ {
102
+ "name": "Brick",
103
+ "actors": [
104
+ {
105
+ "components": {
106
+ "Layout": { "width": 50, "height": 25 },
107
+ "Sprite": { "file": "drawings/brick.pxart" },
108
+ "Collider": { "kind": "solid" },
109
+ "Brick": {}
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ ```
115
+
116
+ Template `components` keys are behavior names (the `static behaviorName`); add a behavior to a kind of actor by adding its key here, remove it by deleting the key. The template almost always has a `Layout` with `width`/`height` (instances supply `x`/`y`).
117
+
118
+ - **Author one blueprint per kind of thing** — `blueprints/<meaningful>.scene` with a real `"name"` — and place instances that reference it. Do this even for a one-off actor: it costs one small file and keeps the deck's blueprint library intentional.
119
+ - Editing a blueprint file changes every instance that doesn't override that prop, across every scene — so one `blueprints/player.scene` referenced from every level stays in sync everywhere. Editing an instance's own `Layout.x`/`y`/`rotation` never touches the blueprint (position/rotation are always instance-local — see `Layout.jsx`'s `propertyMeta`).
120
+ - **Never write inline actors** (full `components`, no `blueprint` field). They aren't a lighter-weight alternative: the editor auto-migrates every such actor into its OWN new blueprint file the first time a human opens the scene — mechanically, one blueprint per actor, no dedup. That migration is a compatibility net for pre-blueprint decks, not an authoring workflow; leaning on it turns 10 inline enemies into 10 junk blueprints.
121
+ - Never invent your own `blueprint` path pointing at a file you didn't also create — a dangling reference resolves to "no template" (the instance's own sparse `components` render alone, missing whatever it expected to inherit).
122
+
123
+ ## Built-in behaviors
124
+
125
+ - **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
126
+ - **Sprite** — `{ file: "drawings/foo.pxart", tint?: "#rrggbbaa", playing?: true, tag?: "", mode?: 'cover'|'fit'|'stretch'|'tile', tileSize?: 50 }`. Renders a `.pxart` pixel-art sprite into the Layout box: scaled up preserving the art's aspect ratio to fill the whole box, cropping whatever overflows a mismatched box (`mode: 'cover'`, the default -- CSS object-fit: cover), scaled preserving the art's aspect ratio and centered so the whole sprite stays visible, letterboxing a mismatched box instead of cropping (`mode: 'fit'` -- CSS object-fit: contain), scaled to fill the box exactly and distorting the art when the aspect ratios don't match (`mode: 'stretch'`), or repeated (`mode: 'tile'`). `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. Animated sprites play automatically; set `playing: false` to hold the first frame, or `tag` to play a named animation tag. Tile mode repeats the art at a fixed cell size (`tileSize` card units tall, width scaled by the art's aspect) instead of stretching it across the Layout box.
127
+ - **Missing sprites fall back to a placeholder.** If `file` names a sprite that doesn't exist yet, it renders the fallback `drawings/cauldron.pxart` — so you can give an actor its REAL intended sprite name (`drawings/paddle.pxart`) right away and it shows the placeholder until that file is created. Reference real names from the start; don't wait for the art.
128
+ - **Fill the box, don't distort.** The default `mode: 'cover'` fills the Layout box with the art undistorted, cropping whatever overflows when the box aspect doesn't match the art's — so treat the Layout box like an image frame in a design tool: size it to frame what matters and keep the sprite's important content toward the center, since the edges may be cropped. Keeping an actor's Layout aspect ratio close to its sprite's native aspect ratio minimizes how much gets cropped. When the whole sprite must stay visible (nothing is safe to crop), use `mode: 'fit'`, which preserves the art and letterboxes the mismatched box instead. Avoid `mode: 'stretch'` for pixel art — filling a long/tall box by distorting wrecks the pixels (a brick sprite stretched into a wall ruins the bricks). For long surfaces (walls, floors, platforms), use `mode: 'tile'` on one actor to repeat the art at a fixed cell size instead of stretching or cropping a single sprite.
129
+ - **Collider** — `{ kind: 'solid'|'pickup', mode?: 'auto'|'manual', width, height, offsetX?, offsetY?, debug? }`. Just a rect; `offsetX`/`offsetY` nudge the collider from its default position (so `0,0` is the default). `mode: 'auto'` (the default) fits the rect to the actor's Sprite's opaque pixels, mapped through the Sprite's draw mode (`fit`'s letterboxed dest rect, `stretch`'s full Layout box, `cover`'s filled box clipped to the Layout box; `tile` mode and a missing/unresolved Sprite use the whole Layout box instead). `width`/`height` only apply in `mode: 'manual'` (a rect of that size, centered in the Layout box); they're ignored in `auto`. The framework does NOT auto-resolve collisions for you. Use it as data:
130
+
131
+ ```jsx
132
+ for (const other of scene.getActors()) {
133
+ if (other.id === actor.id) continue;
134
+ if (scene.overlaps(actor, other)) {
135
+ /* react */
136
+ }
137
+ }
138
+ ```
139
+
140
+ - **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).
141
+
142
+ ## Physics
143
+
144
+ This kit simulates 2D physics with matter-js. You get gravity, collisions,
145
+ bouncing, friction, and forces without hand-writing an integrator. The rule of
146
+ thumb: **give an actor a `Collider` to make it collide; add a `RigidBody` to
147
+ make it move.**
148
+
149
+ ### Making things physical
150
+
151
+ - **`Collider`** — required for any physics participant. Beyond the base fields
152
+ (`mode`, `width`/`height`, `offsetX`/`offsetY`, `debug`) it has:
153
+ - `shape: 'box' | 'circle'` — the physics shape (default `box`). A circle uses
154
+ `radius`, or half the smaller collider dimension if `radius` is 0.
155
+ - `isTrigger` — a **sensor**: detects overlaps (fires collision callbacks) but
156
+ does **not** block. Use for pickups, goals, zones.
157
+ - `bounciness` — restitution, 0 (dead) to ~1 (very bouncy), can exceed 1.
158
+ - `friction` — surface friction (default 0.1).
159
+ - A `Collider` **without** a `RigidBody` is a static obstacle (walls, floors).
160
+ - **`RigidBody`** — makes an actor move under physics:
161
+ - `bodyType: 'dynamic' | 'static' | 'kinematic'`. `dynamic` = simulated
162
+ (gravity, collisions, forces). `static` = never moves (same as a lone
163
+ Collider). `kinematic` = unpushable, but you move it by writing its Layout.
164
+ - `gravityScale` — multiplies world gravity for this body (`0` = floats).
165
+ - `drag` — linear damping (air resistance). `angularDrag` — spin damping.
166
+ - `freezeRotation` — keep it from rotating (stays upright).
167
+ - `velocityX` / `velocityY` — initial velocity, applied once when play starts.
168
+
169
+ ### World gravity
170
+
171
+ Set it per-scene in the `.scene` file: `"physics": { "gravity": 1 }` (matter
172
+ units; `1` is a gentle fall, `0` is zero-g, higher falls faster). `gravityX`
173
+ tilts it sideways. Omit for the default (`1`, downward).
174
+
175
+ ### Driving physics from behaviors — `scene.physics`
176
+
177
+ In a behavior's `update`, steer bodies through `scene.physics` (never write
178
+ `Layout` directly for a dynamic body — the sim owns its transform):
179
+
180
+ - `scene.physics.setVelocity(actor, { x, y })` — set velocity (px per fixed
181
+ step; multiply by ~60 for px/s).
182
+ - `scene.physics.applyImpulse(actor, { x, y })` — add to velocity this frame.
183
+ - `scene.physics.applyForce(actor, { x, y })` — add a matter-space force.
184
+ - `scene.physics.getVelocity(actor)` — current velocity `{ x, y }`.
185
+ - `scene.physics.isColliding(a, b)` — are these two actors touching right now?
186
+
187
+ ### Collision callbacks
188
+
189
+ A behavior can implement these; the physics system calls them on both actors of
190
+ a contact — every pair, including static-vs-static and moving kinematic bodies
191
+ (and sensor overlaps). For a quick one-off "are these two touching right now?"
192
+ without callbacks, use `scene.physics.isColliding(a, b)`. (`scene.overlaps(a, b)`
193
+ also exists but is a cheaper *bounding-box* test — not shape-accurate; prefer
194
+ the physics calls for physics actors.)
195
+
196
+ ```jsx
197
+ export class Coin {
198
+ static behaviorName = 'Coin';
199
+ onCollisionEnter(actor, scene, other) {
200
+ if (other.components.Player) scene.despawnActor(actor.id); // collected
201
+ }
202
+ onCollisionExit(actor, scene, other) {}
203
+ }
204
+ ```
205
+
206
+ ### Built-in controls (add these behaviors, no code needed)
207
+
208
+ These read input and drive the actor through the physics system, so the actor
209
+ still collides with the world. Each needs a `RigidBody` (dynamic) + `Collider`.
210
+ Controls are **touch/pointer-first** (Castle games are played on mobile first;
211
+ touch degrades to mouse). Keyboard, if added, should only *duplicate* an
212
+ on-screen control -- never be the only way to play.
213
+
214
+ - **`Draggable`** — grab the actor with the pointer and fling it (springy,
215
+ collides). `stiffness`.
216
+ - **`Slingshot`** — press the actor, pull back, release to launch it the
217
+ opposite way (angry-birds), with an aim arrow. `speed`, `maxDrag`.
218
+ - **`AnalogStick`** — on-screen virtual joystick (press anywhere) that drives
219
+ the actor. `speed`, `maxRadius`, `axes`. Best for top-down.
220
+
221
+ ### Recipe: a launch-and-bounce game
222
+
223
+ - Ball blueprint: `Layout` (square), `Sprite`, `Collider {shape:'circle',
224
+ bounciness:0.6}`, `RigidBody {bodyType:'dynamic'}`, `Slingshot {}`.
225
+ - Walls/floor blueprint: `Layout`, `Collider {bounciness:0.4}` (no RigidBody →
226
+ static).
227
+ - Goal blueprint: `Collider {isTrigger:true}` + a `Goal` behavior with
228
+ `onCollisionEnter` that scores when `other` is the ball.
229
+ - Scene: set `"physics": { "gravity": 1 }`, place one ball, walls, a goal.
230
+
231
+ ### Gotchas
232
+
233
+ - **Fast bodies + thin walls tunnel.** A small body moving faster than a static
234
+ wall is thick (per simulation step, ~a body-speed of `speed*maxDrag` for a
235
+ Slingshot) can pass straight through it — matter-js has no continuous
236
+ collision. Keep boundary walls thick (50px+) and/or cap launch power. The
237
+ demo scene uses 50px walls with a ~20px/step launch cap.
238
+ - **Don't move a dynamic body by writing `Layout`.** The simulation owns a
239
+ dynamic body's transform and overwrites `Layout` every frame — use
240
+ `scene.physics.setVelocity/applyImpulse/applyForce`. Writing `Layout`
241
+ directly is only for `static`/`kinematic` bodies.
242
+ - **Editor vs play.** Physics only steps during play; in the editor actors sit
243
+ where you place them. `RigidBody.velocityX/Y` apply once when play starts.
244
+
245
+ ## Adding physics to another kit
246
+
247
+ The physics module (`physics/`) is self-contained and portable. To add it to a
248
+ different kit:
249
+
250
+ 1. Copy the `physics/` folder into the kit.
251
+ 2. Add `matter-js` to the kit's `package.json` dependencies.
252
+ 3. In the kit's `engine/scene.js`: add a systems registry to `SceneRuntime`
253
+ (`this.systems = []`, a `registerSystem(system)` method, and, at the end of
254
+ `update(dt)`, `for (const s of this.systems) s.afterBehaviors?.(this, dt);`),
255
+ then call `installPhysics(runtime)` from `makeScene` (and route `clone()`
256
+ through `makeScene` so clones get it too).
257
+ 4. In the kit's `editors/behaviorRegistry`, also glob
258
+ `../physics/behaviors/*.jsx` so the physics behaviors auto-register.
259
+
260
+ The module reuses the kit's `engine/collider.js` `getColliderRect(actor,
261
+ sprites)` for shape geometry, so a kit needs that (both `basic-2d` and this kit
262
+ have it).
263
+
264
+ ## Creating pixel art (`.pxart`)
265
+
266
+ Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a terse svg-rect** — a tiny `<svg viewBox="0 0 16 16">` with **one `<rect>` per pixel** — and pipe it to `npm run draw`. It quantizes every fill to the agent palette and writes a single-frame `drawings/<name>.pxart`.
267
+
268
+ - **Resolution is 16×16.** Use `viewBox="0 0 16 16"` and **one 1×1 `<rect>` per filled pixel** (`width="1" height="1"` at integer `x`/`y`). Leave background pixels out (omitted = transparent). Keep every rect 1×1 — the decoder infers cell size from the smallest rect, so mixing in larger blocks shrinks the whole grid.
269
+ - **Palette is the agent 16-color subset** (a fixed subset of the editor's full Endesga-64 painting palette) — every `fill` must be one of:
270
+
271
+ `#e69c69` `#bf6f4a` `#8a4836` `#391f21` `#891e2b` `#ea323c` `#ffa214` `#ffeb57` `#5ac54f` `#1e6f50` `#134c4c` `#657392` `#c7cfdd` `#ffffff` `#0cf1ff` `#0098dc`
272
+
273
+ (Off-palette fills are snapped to the nearest of these, so stay on-palette to keep what you intend. People editing in the app can paint with the full Endesga-64 palette; only this generation path is constrained to the 16.)
274
+ - **Run it** (sprite name as the arg; stdin is the svg):
275
+
276
+ ```bash
277
+ printf '<svg viewBox="0 0 16 16"><rect x="7" y="4" width="1" height="1" fill="#ffa214"/><rect x="8" y="4" width="1" height="1" fill="#ffa214"/><rect x="7" y="5" width="1" height="1" fill="#0098dc"/><rect x="8" y="5" width="1" height="1" fill="#0098dc"/></svg>' | npm run draw -- ship
278
+ ```
279
+
280
+ writes `drawings/ship.pxart`. You can also pass `--from file.svg` instead of stdin. An undecodable svg (no usable rects) errors with a nonzero exit and writes nothing.
281
+ - **After writing, `npm run restart`.** Newly created files aren't in the kit's static glob until a restart; until then the missing-sprite fallback renders the placeholder (so an actor pointing at a not-yet-drawn `.pxart` is still playable).
282
+
283
+ Point a `Sprite` component's `file` at the generated `drawings/<name>.pxart` to put the art on an actor.
284
+
285
+ ## SceneRuntime API (what `scene` exposes to behaviors)
286
+
287
+ - `scene.time` — seconds since start.
288
+ - `scene.keys` — `Set` of currently-held KeyboardEvent codes (e.g. `'ArrowLeft'`, `'KeyA'`, `'Space'`). Read in `update`.
289
+ - `scene.pointer` — `{ x, y, down }` in world (card) coordinates, camera-adjusted.
290
+ - `scene.getActor(id)` / `scene.getActors()` (sorted by Layout.z) / `scene.getComponent(actor, name)`.
291
+ - `scene.actorWith('GameController')` / `scene.actorsWith('Brick')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
292
+ - `scene.colliderRect(actorOrId)` — rect from Layout + Collider, or null.
293
+ - `scene.overlaps(a, b)` — true when two actors/ids with Collider overlap.
294
+ - `scene.data` — the live scene data. Mutate `actor.components.X = {...}` to change props.
295
+ - `scene.spawnActor({ components: { Layout: {...}, MyBehavior: {...} } })` — add a new actor at runtime with fully-specified components (no blueprint). Returns the actor (with auto-minted `id` and `runtime = {}`). Use this; don't push to `scene.data.actors` by hand.
296
+ - `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.
297
+ - `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
298
+ - `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
299
+ - `scene.load(sceneData)` — replace the running scene with the given scene data object.
300
+ - `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.
301
+ - `scene.loadFromFile(name)` — read a scene file and transition the running scene to it. The way to switch screens/levels and to restart, e.g. `scene.loadFromFile('main.scene')`.
302
+ - `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity, trail history). Not serialized.
303
+
304
+ ## Input shortcuts
305
+
306
+ ```jsx
307
+ if (scene.keys.has('ArrowLeft')) layout.x -= speed * dt;
308
+ if (scene.keys.has('ArrowRight')) layout.x += speed * dt;
309
+ if (scene.keys.has('KeyX')) /* launch ball */ ;
310
+ ```
311
+
312
+ **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.
313
+
314
+ For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`.
315
+
316
+ ## Common breakout-shaped recipe (sketch)
317
+
318
+ - `Paddle` behavior: read keys, clamp x to `[0, 500 - layout.width]`.
319
+ - `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.
320
+ - `Brick` behavior: typically just a marker — `kind: 'solid'` collider is enough. State (hit count) goes on `actor.runtime` or the brick's own props.
321
+ - `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `ui()`.
322
+
323
+ ## Don't
324
+
325
+ - Don't `console.log` in tight loops — flood the serve log.
326
+ - Don't keep per-actor state on the behavior class instance; it's recreated each frame. Use `actor.runtime` or component props.
327
+ - Don't try to import from `editors/`; behaviors run in the play runtime too.
328
+ - Don't add types or `.ts`/`.tsx` files. This kit is JavaScript.
329
+ - Don't add a build step or change `vite.config.js` for a game — it's configured for you.
@@ -0,0 +1,43 @@
1
+ import { cardSize } from '../engine/scene';
2
+
3
+ // Follow camera: keeps the target actor centered in the viewport, clamped to
4
+ // the room bounds so the empty area past the room edges never scrolls in.
5
+ export class Camera {
6
+ static behaviorName = 'Camera';
7
+
8
+ static defaultProps = {
9
+ target: '',
10
+ followX: true,
11
+ followY: true,
12
+ roomWidth: 900,
13
+ roomHeight: 1100,
14
+ };
15
+
16
+ constructor(props) {
17
+ this.props = props;
18
+ }
19
+
20
+ update(_actor, scene) {
21
+ const target = this.props.target ? scene.getActor(this.props.target) : undefined;
22
+ const targetLayout = target?.components.Layout;
23
+ if (!targetLayout) return;
24
+
25
+ const centerX = targetLayout.x + targetLayout.width / 2;
26
+ const centerY = targetLayout.y + targetLayout.height / 2;
27
+ const desiredX = centerX - cardSize.width / 2;
28
+ const desiredY = centerY - cardSize.height / 2;
29
+
30
+ scene.camera = {
31
+ x: this.props.followX
32
+ ? clamp(desiredX, 0, this.props.roomWidth - cardSize.width)
33
+ : 0,
34
+ y: this.props.followY
35
+ ? clamp(desiredY, 0, this.props.roomHeight - cardSize.height)
36
+ : 0,
37
+ };
38
+ }
39
+ }
40
+
41
+ function clamp(value, min, max) {
42
+ return Math.max(min, Math.min(Math.max(min, max), value));
43
+ }
@@ -0,0 +1,199 @@
1
+ import React, { useState } from 'react';
2
+ import { Icon, NumberField, Panel, SelectField } from '../engine/ui';
3
+ import { AutoFields, overrideProps } from '../engine/autoInspector';
4
+ import { computeAutoFit, getColliderRect, getColliderShape, intersects } from '../engine/collider';
5
+
6
+ // Collapsible "Dimensions" section. The header row's label lines up exactly with
7
+ // the field labels above/below (both start at the panel body's 16px left pad);
8
+ // the open/closed caret floats in the left gutter without shifting the label.
9
+ // `margin-bottom: 12px` matches a field row's `padding-bottom`, so the gap to
10
+ // the next entry is the same whether the section is open or closed.
11
+ const dimHeaderRowStyle = {
12
+ position: 'relative',
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ gap: 10,
16
+ minHeight: 28,
17
+ margin: '0 0 12px',
18
+ };
19
+ const dimToggleStyle = {
20
+ display: 'inline-flex',
21
+ alignItems: 'center',
22
+ background: 'none',
23
+ border: 'none',
24
+ padding: 0,
25
+ margin: 0,
26
+ color: 'var(--castle-inspector-text)',
27
+ fontFamily: 'inherit',
28
+ fontSize: 14,
29
+ cursor: 'pointer',
30
+ };
31
+ // The caret sits in the panel's left gutter (label column starts at x=16).
32
+ const dimCaretStyle = {
33
+ position: 'absolute',
34
+ left: -14,
35
+ top: '50%',
36
+ transform: 'translateY(-50%)',
37
+ display: 'inline-flex',
38
+ alignItems: 'center',
39
+ fontSize: 11,
40
+ opacity: 0.65,
41
+ };
42
+ const autoFitLinkStyle = {
43
+ background: 'none',
44
+ border: 'none',
45
+ padding: 0,
46
+ color: '#4aa3ff',
47
+ fontFamily: 'inherit',
48
+ fontSize: 13,
49
+ cursor: 'pointer',
50
+ };
51
+ const dimBodyStyle = { paddingLeft: 14 };
52
+
53
+ export class Collider {
54
+ static behaviorName = 'Collider';
55
+
56
+ static defaultProps = {
57
+ shape: 'box',
58
+ // width/height/radius are intentionally NOT defaulted: an unset size means
59
+ // "match the actor's Layout box" (see engine/collider.js manualRect and the
60
+ // effective values in the inspector). Defaulting them to a fixed number made
61
+ // every collider shrink to that number instead of tracking the actor.
62
+ radius: 0,
63
+ offsetX: 0,
64
+ offsetY: 0,
65
+ // Physics material (used by the physics simulation; ignored by plain
66
+ // scene.overlaps checks). `isTrigger` = a sensor: detects overlaps but
67
+ // doesn't block. `bounciness` = restitution (0..1+). `friction` = surface
68
+ // friction.
69
+ isTrigger: false,
70
+ bounciness: 0,
71
+ friction: 0.1,
72
+ debug: false,
73
+ };
74
+
75
+ constructor(props) {
76
+ this.props = props;
77
+ }
78
+
79
+ draw(actor, scene, ctx, options) {
80
+ if (!options.showDebugColliders && !this.props.debug) return;
81
+ const geom = getColliderShape(actor, scene.sprites);
82
+ if (!geom) return;
83
+ // A sensor (isTrigger) reads as a pass-through zone; a solid as a wall.
84
+ const isSensor = Boolean(this.props.isTrigger) || this.props.kind === 'pickup';
85
+ ctx.save();
86
+ ctx.strokeStyle = isSensor ? '#ffe17a' : '#8db7ff';
87
+ ctx.lineWidth = 2;
88
+ if (geom.shape === 'circle') {
89
+ ctx.beginPath();
90
+ ctx.arc(geom.cx, geom.cy, geom.radius, 0, Math.PI * 2);
91
+ ctx.stroke();
92
+ } else {
93
+ ctx.strokeRect(geom.x + 1, geom.y + 1, geom.width - 2, geom.height - 2);
94
+ }
95
+ ctx.restore();
96
+ }
97
+
98
+ static Inspector({ actor, component, sprites, setComponent, override }) {
99
+ const [dimOpen, setDimOpen] = useState(true);
100
+ const layout = actor?.components?.Layout ?? {};
101
+ const shape = component.shape ?? Collider.defaultProps.shape;
102
+ // Unset width/height/radius track the actor's Layout box; surface that
103
+ // effective value so a field never reads blank while the collider is sized.
104
+ const width = component.width ?? layout.width ?? 50;
105
+ const height = component.height ?? layout.height ?? 50;
106
+ const effectiveRadius = component.radius > 0 ? component.radius : Math.min(width, height) / 2;
107
+
108
+ // "Auto-fit to sprite": the explicit dims/offset that match the sprite's
109
+ // opaque-pixel bounds. Only offered when the collider isn't already fitted.
110
+ const autoFit = computeAutoFit(actor, sprites);
111
+ let autoFitPatch = null;
112
+ if (autoFit) {
113
+ autoFitPatch =
114
+ shape === 'circle'
115
+ ? { radius: Math.round(Math.min(autoFit.width, autoFit.height) / 2), offsetX: autoFit.offsetX, offsetY: autoFit.offsetY }
116
+ : { width: autoFit.width, height: autoFit.height, offsetX: autoFit.offsetX, offsetY: autoFit.offsetY };
117
+ }
118
+ const currentValue = (key) => {
119
+ if (key === 'radius') return effectiveRadius;
120
+ if (key === 'width') return width;
121
+ if (key === 'height') return height;
122
+ return component[key] ?? 0;
123
+ };
124
+ const alreadyFitted =
125
+ autoFitPatch && Object.entries(autoFitPatch).every(([key, value]) => Math.abs(currentValue(key) - value) < 0.6);
126
+
127
+ return (
128
+ <Panel title="Collider" overridden={override?.anyOverridden()}>
129
+ <SelectField
130
+ label="Shape"
131
+ value={component.shape}
132
+ onChange={(value) => setComponent({ shape: value })}
133
+ options={['box', 'circle']}
134
+ {...overrideProps(override, 'shape')}
135
+ />
136
+ <div style={dimHeaderRowStyle}>
137
+ <span style={dimCaretStyle}>
138
+ <Icon name={dimOpen ? 'chevron-down' : 'chevron-right'} />
139
+ </span>
140
+ <button type="button" onClick={() => setDimOpen((open) => !open)} style={dimToggleStyle}>
141
+ Dimensions
142
+ </button>
143
+ {autoFitPatch && !alreadyFitted ? (
144
+ <button type="button" onClick={() => setComponent(autoFitPatch)} style={autoFitLinkStyle}>
145
+ Auto-fit to sprite
146
+ </button>
147
+ ) : null}
148
+ </div>
149
+ {dimOpen ? (
150
+ <div style={dimBodyStyle}>
151
+ {shape === 'circle' ? (
152
+ <NumberField
153
+ label="Radius"
154
+ value={effectiveRadius}
155
+ onChange={(value) => setComponent({ radius: value })}
156
+ {...overrideProps(override, 'radius')}
157
+ />
158
+ ) : (
159
+ <>
160
+ <NumberField
161
+ label="Width"
162
+ value={width}
163
+ onChange={(value) => setComponent({ width: value })}
164
+ {...overrideProps(override, 'width')}
165
+ />
166
+ <NumberField
167
+ label="Height"
168
+ value={height}
169
+ onChange={(value) => setComponent({ height: value })}
170
+ {...overrideProps(override, 'height')}
171
+ />
172
+ </>
173
+ )}
174
+ <AutoFields
175
+ defaultProps={Collider.defaultProps}
176
+ component={component}
177
+ setComponent={setComponent}
178
+ only={['offsetX', 'offsetY']}
179
+ override={override}
180
+ />
181
+ </div>
182
+ ) : null}
183
+ <AutoFields
184
+ defaultProps={Collider.defaultProps}
185
+ component={component}
186
+ setComponent={setComponent}
187
+ exclude={['shape', 'width', 'height', 'radius', 'offsetX', 'offsetY']}
188
+ override={override}
189
+ />
190
+ </Panel>
191
+ );
192
+ }
193
+ }
194
+
195
+ // Re-exported for callers that need the rect / overlap test outside a running
196
+ // scene (e.g. editors/SelectionOverlay.jsx, which passes the merged preview
197
+ // actors plus the sprites map). See engine/collider.js for the single
198
+ // implementation.
199
+ export { getColliderRect, getColliderShape, intersects };
@@ -0,0 +1,29 @@
1
+ // Goal: a sensor zone that counts when a physics body enters it, and flashes
2
+ // green briefly. Demonstrates the physics collision callbacks + a Collider with
3
+ // `isTrigger: true`. Put it on an actor with a Layout and a trigger Collider.
4
+ export class Goal {
5
+ static behaviorName = 'Goal';
6
+
7
+ static defaultProps = {};
8
+
9
+ constructor(props) {
10
+ this.props = props;
11
+ }
12
+
13
+ onCollisionEnter(actor, scene, other) {
14
+ if (!other.components.RigidBody) return; // only score moving bodies
15
+ actor.runtime.scored = (actor.runtime.scored ?? 0) + 1;
16
+ actor.runtime.flashUntil = scene.time + 0.6;
17
+ }
18
+
19
+ draw(actor, scene, ctx) {
20
+ const layout = actor.components.Layout;
21
+ const lit = (actor.runtime.flashUntil ?? 0) > scene.time;
22
+ ctx.save();
23
+ ctx.strokeStyle = lit ? '#5ac54f' : 'rgba(255, 235, 87, 0.9)';
24
+ ctx.lineWidth = 4;
25
+ ctx.setLineDash([6, 4]);
26
+ ctx.strokeRect(layout.x + 2, layout.y + 2, layout.width - 4, layout.height - 4);
27
+ ctx.restore();
28
+ }
29
+ }