castle-web-cli 0.4.129 → 0.4.130

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 (144) hide show
  1. package/dist/agent-prompts.js +4 -4
  2. package/dist/filesChanged.d.ts +2 -0
  3. package/dist/filesChanged.js +6 -2
  4. package/dist/ide.js +15 -4
  5. package/dist/importBrowse.js +1 -0
  6. package/dist/index.js +10 -4
  7. package/dist/init.js +63 -15
  8. package/dist/native/loop.js +1 -1
  9. package/dist/native/tools.js +1 -1
  10. package/dist/serve.d.ts +1 -0
  11. package/dist/serve.js +20 -1
  12. package/dist/shell/assets/index-BpOLUyVO.js +441 -0
  13. package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
  14. package/dist/shell/index.html +2 -2
  15. package/kits/physics-2d/CLAUDE.md +9 -10
  16. package/kits/physics-2d/castle.json +1 -1
  17. package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
  18. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  19. package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
  20. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  21. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  22. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  23. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  24. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  25. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  26. package/kits/physics-2d/engine/blueprint.js +15 -3
  27. package/kits/physics-2d/engine/liveReload.js +33 -21
  28. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  29. package/kits/physics-2d/engine/ui.jsx +2 -2
  30. package/kits/physics-2d/engine/ui.module.css +42 -0
  31. package/kits/physics-3d/CLAUDE.md +109 -0
  32. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  33. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  34. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  35. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  36. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  37. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  38. package/kits/physics-3d/behaviors/Portal.jsx +19 -0
  39. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  40. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  41. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  42. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  43. package/kits/physics-3d/blueprints/crate.scene +29 -0
  44. package/kits/physics-3d/blueprints/door.scene +35 -0
  45. package/kits/physics-3d/blueprints/gem.scene +26 -0
  46. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  47. package/kits/physics-3d/blueprints/platform.scene +23 -0
  48. package/kits/physics-3d/blueprints/player.scene +29 -0
  49. package/kits/physics-3d/blueprints/portal.scene +21 -0
  50. package/kits/physics-3d/blueprints/rock.scene +14 -0
  51. package/kits/physics-3d/blueprints/statue.scene +26 -0
  52. package/kits/physics-3d/blueprints/tree.scene +14 -0
  53. package/kits/physics-3d/blueprints/wall.scene +26 -0
  54. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  55. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  56. package/kits/physics-3d/drawings/crate.pxart +26 -0
  57. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  58. package/kits/physics-3d/drawings/door.pxart +27 -0
  59. package/kits/physics-3d/drawings/face.pxart +26 -0
  60. package/kits/physics-3d/drawings/floor.pxart +27 -0
  61. package/kits/physics-3d/drawings/platform.pxart +27 -0
  62. package/kits/physics-3d/drawings/statue.pxart +27 -0
  63. package/kits/physics-3d/drawings/wall.pxart +27 -0
  64. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
  65. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  66. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  67. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  68. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  69. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  70. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  71. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  72. package/kits/physics-3d/engine3d/materials.js +174 -0
  73. package/kits/physics-3d/engine3d/meshops.js +123 -0
  74. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  75. package/kits/physics-3d/engine3d/modelFiles.js +139 -0
  76. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  77. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  78. package/kits/physics-3d/engine3d/world3d.js +216 -0
  79. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  80. package/kits/physics-3d/index.html +20 -0
  81. package/kits/physics-3d/main.jsx +30 -0
  82. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  83. package/kits/physics-3d/models/player.pxmodel +222 -0
  84. package/kits/physics-3d/models/rock.pxmodel +30 -0
  85. package/kits/physics-3d/models/tree.pxmodel +10 -0
  86. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  87. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  88. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  89. package/kits/physics-3d/scenes/main.scene +414 -0
  90. package/kits/physics-3d/scenes/model-lab.scene +127 -0
  91. package/kits/physics-3d/systems/physics3d.js +356 -0
  92. package/package.json +5 -2
  93. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  94. package/kits/basic-2d/CLAUDE.md +0 -221
  95. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  96. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  97. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  98. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  99. package/kits/basic-2d/behaviors/tint.js +0 -47
  100. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  101. package/kits/basic-2d/docs/pxart-format.md +0 -377
  102. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  103. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  104. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  105. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  106. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  107. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  108. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  109. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  110. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  111. package/kits/basic-2d/editors/editorHistory.js +0 -157
  112. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  113. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  114. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  115. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  116. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  117. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  118. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  119. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  120. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  121. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  122. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  123. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  124. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  125. package/kits/basic-2d/engine/assets.js +0 -15
  126. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  127. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  128. package/kits/basic-2d/engine/blueprint.js +0 -557
  129. package/kits/basic-2d/engine/collider.js +0 -200
  130. package/kits/basic-2d/engine/files.js +0 -141
  131. package/kits/basic-2d/engine/liveReload.js +0 -88
  132. package/kits/basic-2d/engine/pxart.js +0 -1032
  133. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  134. package/kits/basic-2d/engine/scene.js +0 -696
  135. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  136. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  137. package/kits/basic-2d/engine/ui.jsx +0 -695
  138. package/kits/basic-2d/engine/ui.module.css +0 -2287
  139. package/kits/basic-2d/index.html +0 -24
  140. package/kits/basic-2d/main.jsx +0 -24
  141. package/kits/basic-2d/scenes/main.scene +0 -16
  142. package/kits/basic-2d/scripts/draw.mjs +0 -121
  143. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  144. /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
@@ -1,221 +0,0 @@
1
- # basic-2d kit
2
-
3
- ## Welcome message
4
-
5
- Welcome to an early test of Castle's new engine! 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.
6
-
7
- Do you already know what you want to make, or do you want to figure it out together?
8
-
9
- ## Quick reference
10
-
11
- <!-- Injected into Castle's create assistant every turn; keep this compact and factual. -->
12
-
13
- - Actor / behavior / scene framework on a fixed 500x700 canvas ("card").
14
- - Game logic lives in `behaviors/*.jsx` classes. A behavior's `static behaviorName` must match the component key used in scene JSON.
15
- - Editable screens live in `scenes/*.scene` files (plain JSON). Use a separate scene file per distinct screen, and switch with `scene.loadFromFile('name.scene')`.
16
- - 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.
17
- - 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.
18
- - This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
19
- - After any code, scene, or drawing edit, run `npm run restart`.
20
- - Space is reserved by the editor for play/stop; do not bind Space to gameplay.
21
- - 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.
22
- - Naming files: a plain path (`drawings/ship.pxart`) is a file of THIS deck; `@imports/<alias>/...` is a file of a deck this one imports. Same in JS (`import x from '@imports/someone.pack/thing.js'`), in scene/blueprint refs, and in `Sprite.file`. See `## Files and imports`.
23
- - Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
24
-
25
- ## Files and imports
26
-
27
- One rule wherever a file is named -- JS imports, `"blueprint"` refs, `Sprite.file`,
28
- anything a behavior invents:
29
-
30
- - `drawings/ship.pxart` -- a file of the deck the reference is WRITTEN IN. In this
31
- deck's own files that means this deck; in a file belonging to an import, that
32
- import. So a kit's blueprint saying `drawings/cauldron.pxart` keeps meaning the
33
- kit's drawing once the kit is imported by someone else.
34
- - `@imports/<alias>/drawings/ship.pxart` -- a file of the deck imported under
35
- `<alias>`. This is the only way to name another deck's file, so cross-deck
36
- references are visible as such, and it means the same thing from any file at
37
- any depth.
38
-
39
- Imports are read-only: their files belong to the deck they came from. Use them,
40
- don't edit them. `castle-web add-import <deckId>` adds one, `update-import`
41
- re-fetches it. `resolveDeckFile` from `castle-web-sdk` is the rule itself, if a
42
- behavior needs to resolve a path it was handed.
43
-
44
- ## Scope
45
-
46
- 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.
47
-
48
- ## Workflow
49
-
50
- 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:
51
-
52
- 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.
53
- 2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
54
- 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.
55
- 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`.
56
-
57
- Card size is **500 wide × 700 tall** (origin top-left, +y is down).
58
-
59
- ## Behavior shape
60
-
61
- A behavior is a class. Minimal contract:
62
-
63
- ```jsx
64
- // behaviors/MyThing.jsx
65
- export class MyThing {
66
- static behaviorName = 'MyThing'; // must match the key used in .scene
67
- static defaultProps = { speed: 200 };
68
-
69
- constructor(props) {
70
- this.props = props;
71
- }
72
-
73
- // Called every frame in play mode. dt is seconds.
74
- update(actor, scene, dt) {
75
- const layout = actor.components.Layout;
76
- layout.x += this.props.speed * dt; // mutate component in place
77
- }
78
-
79
- // Optional. Custom drawing (you usually don't need this; use a Sprite
80
- // component instead). ctx is in card units already.
81
- draw(actor, scene, ctx) {}
82
-
83
- // Optional. Return React nodes for game-time HUD. Coordinates are card
84
- // units. Read state your `update` set; do not start your own loops.
85
- ui(actor, scene) {
86
- return null;
87
- }
88
- }
89
- ```
90
-
91
- 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.
92
-
93
- ## Scene file (`scenes/main.scene`, plain JSON)
94
-
95
- 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:
96
-
97
- ```json
98
- {
99
- "background": "#1b2030",
100
- "actors": [
101
- { "id": "paddle", "blueprint": "blueprints/paddle.scene", "components": { "Layout": { "x": 200, "y": 650 } } },
102
- { "id": "brick-1", "blueprint": "blueprints/brick.scene", "components": { "Layout": { "x": 50, "y": 75 } } },
103
- { "id": "brick-2", "blueprint": "blueprints/brick.scene", "components": { "Layout": { "x": 100, "y": 75 } } }
104
- ]
105
- }
106
- ```
107
-
108
- 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.
109
-
110
- ## Blueprints
111
-
112
- A **blueprint** is a `.scene` file under `blueprints/` whose single actor (`actors[0]`, no `id`) is the template:
113
-
114
- ```json
115
- {
116
- "name": "Brick",
117
- "actors": [
118
- {
119
- "components": {
120
- "Layout": { "width": 50, "height": 25 },
121
- "Sprite": { "file": "drawings/brick.pxart" },
122
- "Collider": {},
123
- "Brick": {}
124
- }
125
- }
126
- ]
127
- }
128
- ```
129
-
130
- 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`).
131
-
132
- - **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.
133
- - 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`).
134
- - **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.
135
- - 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).
136
-
137
- ## Built-in behaviors
138
-
139
- - **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
140
- - **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.
141
- - **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.
142
- - **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.
143
- - **Collider** — `{ shape?: 'box'|'circle', width, height, radius, offsetX?, offsetY?, isTrigger?, debug? }`. A box (default) or circle. **Size tracks the Layout box** unless you set an explicit `width`/`height` (box) or `radius` (circle); `offsetX`/`offsetY` nudge it from center (so `0,0` is centered). In the editor, the Collider panel's **"Auto-fit to sprite"** action snaps the dimensions/offset to the sprite's opaque-pixel bounds (only shown when it isn't already fitted) — a one-time computed value, not a live mode. `isTrigger: true` marks a **sensor** (draws yellow, reads as a pass-through zone for pickup/goal logic); a plain collider reads as a solid wall. Either way the framework does NOT auto-resolve collisions — the collider is data you act on. (Legacy decks carrying `mode: 'auto'` keep their old live sprite-fit for back-compat.) Use it:
144
-
145
- ```jsx
146
- for (const other of scene.getActors()) {
147
- if (other.id === actor.id) continue;
148
- if (scene.overlaps(actor, other)) {
149
- /* react */
150
- }
151
- }
152
- ```
153
-
154
- - **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).
155
-
156
- ## Creating pixel art (`.pxart`)
157
-
158
- 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`.
159
-
160
- - **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.
161
- - **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:
162
-
163
- `#e69c69` `#bf6f4a` `#8a4836` `#391f21` `#891e2b` `#ea323c` `#ffa214` `#ffeb57` `#5ac54f` `#1e6f50` `#134c4c` `#657392` `#c7cfdd` `#ffffff` `#0cf1ff` `#0098dc`
164
-
165
- (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.)
166
- - **Run it** (sprite name as the arg; stdin is the svg):
167
-
168
- ```bash
169
- 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
170
- ```
171
-
172
- 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.
173
- - **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).
174
-
175
- Point a `Sprite` component's `file` at the generated `drawings/<name>.pxart` to put the art on an actor.
176
-
177
- ## SceneRuntime API (what `scene` exposes to behaviors)
178
-
179
- - `scene.time` — seconds since start.
180
- - `scene.keys` — `Set` of currently-held KeyboardEvent codes (e.g. `'ArrowLeft'`, `'KeyA'`, `'Space'`). Read in `update`.
181
- - `scene.pointer` — `{ x, y, down }` in world (card) coordinates, camera-adjusted.
182
- - `scene.getActor(id)` / `scene.getActors()` (sorted by Layout.z) / `scene.getComponent(actor, name)`.
183
- - `scene.actorWith('GameController')` / `scene.actorsWith('Brick')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
184
- - `scene.colliderRect(actorOrId)` — rect from Layout + Collider, or null.
185
- - `scene.overlaps(a, b)` — true when two actors/ids with Collider overlap.
186
- - `scene.data` — the live scene data. Mutate `actor.components.X = {...}` to change props.
187
- - `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.
188
- - `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.
189
- - `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
190
- - `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
191
- - `scene.load(sceneData)` — replace the running scene with the given scene data object.
192
- - `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.
193
- - `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')`.
194
- - `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity, trail history). Not serialized.
195
-
196
- ## Input shortcuts
197
-
198
- ```jsx
199
- if (scene.keys.has('ArrowLeft')) layout.x -= speed * dt;
200
- if (scene.keys.has('ArrowRight')) layout.x += speed * dt;
201
- if (scene.keys.has('KeyX')) /* launch ball */ ;
202
- ```
203
-
204
- **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.
205
-
206
- For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`.
207
-
208
- ## Common breakout-shaped recipe (sketch)
209
-
210
- - `Paddle` behavior: read keys, clamp x to `[0, 500 - layout.width]`.
211
- - `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.
212
- - `Brick` behavior: typically just a marker — a plain `Collider` is enough. State (hit count) goes on `actor.runtime` or the brick's own props.
213
- - `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `ui()`.
214
-
215
- ## Don't
216
-
217
- - Don't `console.log` in tight loops — flood the serve log.
218
- - Don't keep per-actor state on the behavior class instance; it's recreated each frame. Use `actor.runtime` or component props.
219
- - Don't try to import from `editors/`; behaviors run in the play runtime too.
220
- - Don't add types or `.ts`/`.tsx` files. This kit is JavaScript.
221
- - Don't add a build step or change `vite.config.js` for a game — it's configured for you.
@@ -1,43 +0,0 @@
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
- }
@@ -1,213 +0,0 @@
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
- import { extensionDefaultProps } from '../engine/behaviorExtensions';
6
-
7
- // Collapsible "Dimensions" section. The header row's label lines up exactly with
8
- // the field labels above/below (both start at the panel body's 16px left pad);
9
- // the open/closed caret floats in the left gutter without shifting the label.
10
- // `margin-bottom: 12px` matches a field row's `padding-bottom`, so the gap to
11
- // the next entry is the same whether the section is open or closed.
12
- const dimHeaderRowStyle = {
13
- position: 'relative',
14
- display: 'flex',
15
- alignItems: 'center',
16
- gap: 10,
17
- minHeight: 28,
18
- margin: '0 0 12px',
19
- };
20
- const dimToggleStyle = {
21
- display: 'inline-flex',
22
- alignItems: 'center',
23
- background: 'none',
24
- border: 'none',
25
- padding: 0,
26
- margin: 0,
27
- color: 'var(--castle-inspector-text)',
28
- fontFamily: 'inherit',
29
- fontSize: 14,
30
- cursor: 'pointer',
31
- };
32
- // The caret sits in the panel's left gutter (label column starts at x=16).
33
- const dimCaretStyle = {
34
- position: 'absolute',
35
- left: -14,
36
- top: '50%',
37
- transform: 'translateY(-50%)',
38
- display: 'inline-flex',
39
- alignItems: 'center',
40
- fontSize: 11,
41
- opacity: 0.65,
42
- };
43
- const autoFitLinkStyle = {
44
- background: 'none',
45
- border: 'none',
46
- padding: 0,
47
- color: '#4aa3ff',
48
- fontFamily: 'inherit',
49
- fontSize: 13,
50
- cursor: 'pointer',
51
- };
52
- const dimBodyStyle = { paddingLeft: 14 };
53
-
54
- export class Collider {
55
- static behaviorName = 'Collider';
56
-
57
- static defaultProps = {
58
- shape: 'box',
59
- // width/height/radius are intentionally NOT defaulted: an unset size means
60
- // "match the actor's Layout box" (see engine/collider.js manualRect and the
61
- // effective values in the inspector). Defaulting them to a fixed number made
62
- // every collider shrink to that number instead of tracking the actor.
63
- radius: 0,
64
- offsetX: 0,
65
- offsetY: 0,
66
- // `isTrigger` marks a sensor: it still reports overlaps (draws yellow, reads
67
- // as a pass-through zone for pickup/goal logic) but does not block on its
68
- // own -- collisions are data you act on. Kit modules can register MORE
69
- // Collider fields from outside this file (the physics module adds
70
- // bounciness/friction material); see engine/behaviorExtensions.js. That's
71
- // why this file is byte-identical across kits -- the physics-only fields
72
- // live in physics/extensions/, not here.
73
- isTrigger: false,
74
- debug: false,
75
- ...extensionDefaultProps('Collider'),
76
- };
77
-
78
- constructor(props) {
79
- this.props = props;
80
- }
81
-
82
- // Seed props when the collider is first added to an actor: snap it to the
83
- // sprite's opaque-pixel bounds (what "Auto-fit to sprite" computes) so a new
84
- // collider frames the art rather than the whole Layout box. Falls back to the
85
- // unset (Layout-box) size when there's nothing to fit to -- no sprite, tile
86
- // mode, or transparent art. The editor's addBehavior calls this (see
87
- // editors/SceneEditor.jsx initialBehaviorProps).
88
- static initialProps(actor, ctx) {
89
- const fit = computeAutoFit(actor, ctx?.sprites);
90
- return fit ? { ...Collider.defaultProps, ...fit } : { ...Collider.defaultProps };
91
- }
92
-
93
- draw(actor, scene, ctx, options) {
94
- if (!options.showDebugColliders && !this.props.debug) return;
95
- const geom = getColliderShape(actor, scene.sprites);
96
- if (!geom) return;
97
- // A sensor (isTrigger) reads as a pass-through zone; a solid as a wall.
98
- const isSensor = Boolean(this.props.isTrigger) || this.props.kind === 'pickup';
99
- ctx.save();
100
- ctx.strokeStyle = isSensor ? '#ffe17a' : '#8db7ff';
101
- ctx.lineWidth = 2;
102
- if (geom.shape === 'circle') {
103
- ctx.beginPath();
104
- ctx.arc(geom.cx, geom.cy, geom.radius, 0, Math.PI * 2);
105
- ctx.stroke();
106
- } else {
107
- ctx.strokeRect(geom.x + 1, geom.y + 1, geom.width - 2, geom.height - 2);
108
- }
109
- ctx.restore();
110
- }
111
-
112
- static Inspector({ actor, component, sprites, setComponent, override }) {
113
- const [dimOpen, setDimOpen] = useState(true);
114
- const layout = actor?.components?.Layout ?? {};
115
- const shape = component.shape ?? Collider.defaultProps.shape;
116
- // Unset width/height/radius track the actor's Layout box; surface that
117
- // effective value so a field never reads blank while the collider is sized.
118
- const width = component.width ?? layout.width ?? 50;
119
- const height = component.height ?? layout.height ?? 50;
120
- const effectiveRadius = component.radius > 0 ? component.radius : Math.min(width, height) / 2;
121
-
122
- // "Auto-fit to sprite": the explicit dims/offset that match the sprite's
123
- // opaque-pixel bounds. Only offered when the collider isn't already fitted.
124
- const autoFit = computeAutoFit(actor, sprites);
125
- let autoFitPatch = null;
126
- if (autoFit) {
127
- autoFitPatch =
128
- shape === 'circle'
129
- ? { radius: Math.round(Math.min(autoFit.width, autoFit.height) / 2), offsetX: autoFit.offsetX, offsetY: autoFit.offsetY }
130
- : { width: autoFit.width, height: autoFit.height, offsetX: autoFit.offsetX, offsetY: autoFit.offsetY };
131
- }
132
- const currentValue = (key) => {
133
- if (key === 'radius') return effectiveRadius;
134
- if (key === 'width') return width;
135
- if (key === 'height') return height;
136
- return component[key] ?? 0;
137
- };
138
- const alreadyFitted =
139
- autoFitPatch && Object.entries(autoFitPatch).every(([key, value]) => Math.abs(currentValue(key) - value) < 0.6);
140
-
141
- return (
142
- <Panel title="Collider" overridden={override?.anyOverridden()}>
143
- <SelectField
144
- label="Shape"
145
- value={component.shape}
146
- onChange={(value) => setComponent({ shape: value })}
147
- options={['box', 'circle']}
148
- {...overrideProps(override, 'shape')}
149
- />
150
- <div style={dimHeaderRowStyle}>
151
- <span style={dimCaretStyle}>
152
- <Icon name={dimOpen ? 'chevron-down' : 'chevron-right'} />
153
- </span>
154
- <button type="button" onClick={() => setDimOpen((open) => !open)} style={dimToggleStyle}>
155
- Dimensions
156
- </button>
157
- {autoFitPatch && !alreadyFitted ? (
158
- <button type="button" onClick={() => setComponent(autoFitPatch)} style={autoFitLinkStyle}>
159
- Auto-fit to sprite
160
- </button>
161
- ) : null}
162
- </div>
163
- {dimOpen ? (
164
- <div style={dimBodyStyle}>
165
- {shape === 'circle' ? (
166
- <NumberField
167
- label="Radius"
168
- value={effectiveRadius}
169
- onChange={(value) => setComponent({ radius: value })}
170
- {...overrideProps(override, 'radius')}
171
- />
172
- ) : (
173
- <>
174
- <NumberField
175
- label="Width"
176
- value={width}
177
- onChange={(value) => setComponent({ width: value })}
178
- {...overrideProps(override, 'width')}
179
- />
180
- <NumberField
181
- label="Height"
182
- value={height}
183
- onChange={(value) => setComponent({ height: value })}
184
- {...overrideProps(override, 'height')}
185
- />
186
- </>
187
- )}
188
- <AutoFields
189
- defaultProps={Collider.defaultProps}
190
- component={component}
191
- setComponent={setComponent}
192
- only={['offsetX', 'offsetY']}
193
- override={override}
194
- />
195
- </div>
196
- ) : null}
197
- <AutoFields
198
- defaultProps={Collider.defaultProps}
199
- component={component}
200
- setComponent={setComponent}
201
- exclude={['shape', 'width', 'height', 'radius', 'offsetX', 'offsetY']}
202
- override={override}
203
- />
204
- </Panel>
205
- );
206
- }
207
- }
208
-
209
- // Re-exported for callers that need the rect / overlap test outside a running
210
- // scene (e.g. editors/SelectionOverlay.jsx, which passes the merged preview
211
- // actors plus the sprites map). See engine/collider.js for the single
212
- // implementation.
213
- export { getColliderRect, getColliderShape, intersects };
@@ -1,53 +0,0 @@
1
- export class Layout {
2
- static behaviorName = 'Layout';
3
-
4
- static defaultProps = {
5
- x: 0,
6
- y: 0,
7
- z: 0,
8
- rotation: 0,
9
- width: 50,
10
- height: 50,
11
- // Show the Layout box (the invisible frame the sprite fills and the collider
12
- // derives from) as a dashed outline, in editor and play. Per-actor toggle.
13
- debug: false,
14
- };
15
-
16
- // Position/rotation/z never inherit from a blueprint: they're always written
17
- // as instance values at placement time (z gets an explicit top-of-stack value
18
- // on spawn), and a blueprint edit to its own template x/y/z/rotation only
19
- // affects newly placed instances, never moves ones already placed. Because
20
- // they're always instance-local, they're also never surfaced as blueprint
21
- // "overrides" in the inspector. width/height default to inherited (absent).
22
- //
23
- // Size is width/height in card units -- the same space you compose in. A
24
- // user-facing scaleX/scaleY (relative to the sprite's native pixels) was
25
- // tried and reverted: it only pays off under pixel-perfect (integer scales),
26
- // and in today's card-units model it just adds a conversion + couples Layout
27
- // to the Sprite. Revisit scale fields when pixel-perfect mode lands. See
28
- // ~/castle/cauldron-rendering-scale-model.md.
29
- static propertyMeta = {
30
- x: { inherit: false },
31
- y: { inherit: false },
32
- z: { inherit: false },
33
- rotation: { inherit: false },
34
- };
35
-
36
- constructor(props) {
37
- this.props = props;
38
- }
39
-
40
- // Outline the Layout box when `debug` is on. Drawn in the actor's own rotated
41
- // frame (so it rotates with the actor), with a distinct dashed lavender style
42
- // so it reads separately from the solid collider debug and the selection box.
43
- draw(actor, scene, ctx) {
44
- if (!this.props.debug) return;
45
- const { x, y, width, height } = this.props;
46
- ctx.save();
47
- ctx.strokeStyle = 'rgba(190, 178, 255, 0.75)';
48
- ctx.setLineDash([4, 3]);
49
- ctx.lineWidth = 1.5;
50
- ctx.strokeRect(x + 0.5, y + 0.5, width - 1, height - 1);
51
- ctx.restore();
52
- }
53
- }