castle-web-cli 0.4.114 → 0.4.116
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.js +2 -2
- package/dist/agent.d.ts +9 -9
- package/dist/agent.js +590 -589
- package/dist/castleJson.d.ts +7 -0
- package/dist/castleJson.js +10 -0
- package/dist/editorConfig.d.ts +34 -0
- package/dist/editorConfig.js +125 -0
- package/dist/ide.js +245 -120
- package/dist/imports.d.ts +10 -0
- package/dist/imports.js +149 -17
- package/dist/init.d.ts +3 -0
- package/dist/init.js +113 -88
- package/dist/install.d.ts +1 -1
- package/dist/install.js +26 -24
- package/dist/serve.js +16 -0
- package/dist/shell/assets/index-DiPlPGyg.js +144 -0
- package/dist/shell/assets/index-Y6cJRRCX.css +1 -0
- package/dist/shell/index.html +2 -2
- package/dist/unsupportedMedia.d.ts +1 -0
- package/dist/unsupportedMedia.js +54 -0
- package/dist/vitePlugins.js +13 -20
- package/kits/basic-2d/castle.json +1 -1
- package/kits/basic-2d/editors/behaviorRegistry.js +5 -7
- package/kits/physics-2d/CLAUDE.md +65 -34
- package/kits/physics-2d/{physics/behaviors → behaviors}/AnalogStick.jsx +3 -3
- package/kits/physics-2d/behaviors/Collider.jsx +1 -1
- package/kits/physics-2d/{physics/behaviors → behaviors}/Draggable.jsx +3 -3
- package/kits/physics-2d/{physics/behaviors → behaviors}/Joints.jsx +5 -5
- package/kits/physics-2d/{physics/behaviors → behaviors}/RigidBody.jsx +2 -2
- package/kits/physics-2d/{physics/behaviors → behaviors}/Slingshot.jsx +3 -3
- package/kits/physics-2d/behaviors/Sound.jsx +152 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +112 -42
- package/kits/physics-2d/behaviors/Tone.jsx +83 -0
- package/kits/physics-2d/behaviors/Video.jsx +111 -0
- package/kits/physics-2d/behaviors/tint.js +24 -9
- package/kits/physics-2d/blueprints/ball.scene +1 -1
- package/kits/physics-2d/blueprints/block.scene +1 -1
- package/kits/physics-2d/blueprints/cauldron.scene +1 -1
- package/kits/physics-2d/blueprints/crate.scene +1 -1
- package/kits/physics-2d/castle.json +74 -4
- package/kits/physics-2d/docs/pxart-format.md +537 -51
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +7 -3
- package/kits/physics-2d/editors/ImageViewer.jsx +206 -0
- package/kits/physics-2d/editors/MediaPlayer.jsx +57 -0
- package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
- package/kits/physics-2d/editors/SceneEditor.jsx +7 -3
- package/kits/physics-2d/editors/SingleEditor.jsx +8 -0
- package/kits/physics-2d/editors/behaviorRegistry.js +5 -7
- package/kits/physics-2d/editors/brushFit.js +535 -0
- package/kits/physics-2d/editors/brushShapes.js +140 -0
- package/kits/physics-2d/editors/mediaFile.js +31 -0
- package/kits/physics-2d/editors/mediaViewer.module.css +128 -0
- package/kits/physics-2d/editors/pathOverlay.js +340 -0
- package/kits/physics-2d/editors/pathTools.js +1906 -0
- package/kits/physics-2d/editors/pixelCanvas.js +13 -0
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +2 -2
- package/kits/physics-2d/editors/pixelGeometry.js +4 -2
- package/kits/physics-2d/editors/pixelInspector.jsx +410 -37
- package/kits/physics-2d/editors/pxArtEditorModel.js +172 -16
- package/kits/physics-2d/editors/pxArtTimeline.jsx +163 -43
- package/kits/physics-2d/editors/pxArtTimeline.module.css +31 -5
- package/kits/physics-2d/engine/ScenePlayer.jsx +1 -0
- package/kits/physics-2d/engine/art.js +105 -0
- package/kits/physics-2d/engine/assets.js +12 -4
- package/kits/physics-2d/engine/audioContext.js +34 -0
- package/kits/physics-2d/engine/blueprint.js +3 -3
- package/kits/physics-2d/engine/collider.js +28 -17
- package/kits/physics-2d/engine/files.js +77 -1
- package/kits/physics-2d/engine/liveReload.js +1 -1
- package/kits/physics-2d/engine/media.js +212 -0
- package/kits/physics-2d/{physics → engine/physics}/PhysicsSystem.js +1 -1
- package/kits/physics-2d/{physics → engine/physics}/jointArt.js +3 -4
- package/kits/physics-2d/{physics → engine/physics}/matterBridge.js +2 -9
- package/kits/physics-2d/engine/pxart.js +153 -35
- package/kits/physics-2d/engine/pxartPath.js +1356 -0
- package/kits/physics-2d/engine/pxartSmooth.js +276 -125
- package/kits/physics-2d/engine/scene.js +12 -0
- package/kits/physics-2d/engine/tone.js +112 -0
- package/kits/physics-2d/engine/ui.jsx +22 -1
- package/kits/physics-2d/engine/ui.module.css +36 -12
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-2d/scripts/draw.mjs +7 -7
- package/kits/physics-2d/scripts/import-svg.mjs +1231 -0
- package/kits/physics-2d/scripts/svg-emission-guide.md +92 -0
- package/kits/physics-2d/systems/media.js +34 -0
- package/kits/physics-2d/systems/physics.js +12 -3
- package/package.json +1 -1
- package/dist/shell/assets/index-CqpY1xZR.js +0 -144
- package/dist/shell/assets/index-DCwVFL5u.css +0 -1
- package/kits/physics-2d/physics/index.js +0 -26
- /package/kits/physics-2d/drawings/{block.pxart → block.sprite} +0 -0
- /package/kits/physics-2d/drawings/{cauldron.pxart → cauldron.sprite} +0 -0
- /package/kits/physics-2d/drawings/{joint-rope.pxart → joint-rope.sprite} +0 -0
- /package/kits/physics-2d/{physics → engine/physics}/controls.js +0 -0
- /package/kits/physics-2d/{physics → engine/physics}/joints.js +0 -0
|
@@ -9,7 +9,7 @@ See `## Physics` below.
|
|
|
9
9
|
|
|
10
10
|
## Welcome message
|
|
11
11
|
|
|
12
|
-
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.
|
|
12
|
+
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.sprite` 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.
|
|
13
13
|
|
|
14
14
|
Do you already know what you want to make, or do you want to figure it out together?
|
|
15
15
|
|
|
@@ -21,11 +21,13 @@ Do you already know what you want to make, or do you want to figure it out toget
|
|
|
21
21
|
- Game logic lives in `behaviors/*.jsx` classes. A behavior's `static behaviorName` must match the component key used in scene JSON.
|
|
22
22
|
- Editable screens live in `scenes/*.scene` files (plain JSON). Use a separate scene file per distinct screen, and switch with `scene.loadFromFile('name.scene')`.
|
|
23
23
|
- 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.
|
|
24
|
-
- Real game objects and scenery should usually be editable sprites: generate `.
|
|
24
|
+
- 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.
|
|
25
|
+
- A deck can also hold uploaded media (the Files panel's Upload button): `Sprite` draws image files as well as `.sprite`, `Sound` and `Video` play audio and video files, and `Tone` synthesizes a note with no file at all. Point their `file` at the deck path (`assets/logo.png`).
|
|
26
|
+
- **Supported media formats are exactly**: `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` images, `.mp3` `.wav` `.m4a` audio, `.mp4` (H.264) video. Nothing else — a published deck inlines every asset as a `data:` URI, where the browser trusts the declared type instead of sniffing the bytes, so a format that merely works while serving (`.mov` is the classic case) can be dead once published. Convert, don't improvise.
|
|
25
27
|
- This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
|
|
26
28
|
- After any code, scene, or drawing edit, run `npm run restart`.
|
|
27
29
|
- Space is reserved by the editor for play/stop; do not bind Space to gameplay.
|
|
28
|
-
- 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.
|
|
30
|
+
- 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.
|
|
29
31
|
- Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
|
|
30
32
|
|
|
31
33
|
## Files and imports
|
|
@@ -33,11 +35,11 @@ Do you already know what you want to make, or do you want to figure it out toget
|
|
|
33
35
|
One rule wherever a file is named -- JS imports, `"blueprint"` refs, `Sprite.file`,
|
|
34
36
|
a joint's `sprite`, anything a behavior invents:
|
|
35
37
|
|
|
36
|
-
- `drawings/ship.
|
|
38
|
+
- `drawings/ship.sprite` -- a file of the deck the reference is WRITTEN IN. In this
|
|
37
39
|
deck's own files that means this deck; in a file belonging to an import, that
|
|
38
|
-
import. So a kit's blueprint saying `drawings/cauldron.
|
|
40
|
+
import. So a kit's blueprint saying `drawings/cauldron.sprite` keeps meaning the
|
|
39
41
|
kit's drawing once the kit is imported by someone else.
|
|
40
|
-
- `@imports/<alias>/drawings/ship.
|
|
42
|
+
- `@imports/<alias>/drawings/ship.sprite` -- a file of the deck imported under
|
|
41
43
|
`<alias>`. This is the only way to name another deck's file, so cross-deck
|
|
42
44
|
references are visible as such, and it means the same thing from any file at
|
|
43
45
|
any depth.
|
|
@@ -57,7 +59,7 @@ The deck is already serving when you start (`castle-web init` set that up; see `
|
|
|
57
59
|
|
|
58
60
|
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.
|
|
59
61
|
2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
|
|
60
|
-
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 `.
|
|
62
|
+
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 `.sprite` 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.
|
|
61
63
|
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`.
|
|
62
64
|
|
|
63
65
|
Card size is **500 wide × 700 tall** (origin top-left, +y is down).
|
|
@@ -124,7 +126,7 @@ A **blueprint** is a `.scene` file under `blueprints/` whose single actor (`acto
|
|
|
124
126
|
{
|
|
125
127
|
"components": {
|
|
126
128
|
"Layout": { "width": 50, "height": 25 },
|
|
127
|
-
"Sprite": { "file": "drawings/brick.
|
|
129
|
+
"Sprite": { "file": "drawings/brick.sprite" },
|
|
128
130
|
"Collider": { "kind": "solid" },
|
|
129
131
|
"Brick": {}
|
|
130
132
|
}
|
|
@@ -143,8 +145,8 @@ Template `components` keys are behavior names (the `static behaviorName`); add a
|
|
|
143
145
|
## Built-in behaviors
|
|
144
146
|
|
|
145
147
|
- **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
|
|
146
|
-
- **Sprite** — `{ file: "drawings/foo.
|
|
147
|
-
- **Missing sprites fall back to a placeholder.** If `file` names a sprite that doesn't exist yet, it renders the fallback `drawings/cauldron.
|
|
148
|
+
- **Sprite** — `{ file: "drawings/foo.sprite", tint?: "#rrggbbaa", playing?: true, tag?: "", mode?: 'cover'|'fit'|'stretch'|'tile', tileSize?: 50 }`. Renders the deck's 2D art into the Layout box. `file` is either a `.sprite` pixel-art sprite or an uploaded **image file** (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`) — every prop below works the same on both, except that an image is a single still (no frames or tags) and is drawn smoothed rather than nearest-neighbour, which is what you want for a photo and not for pixel art. Draw modes: 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.
|
|
149
|
+
- **Missing sprites fall back to a placeholder.** If `file` names a sprite that doesn't exist yet, it renders the fallback `drawings/cauldron.sprite` — so you can give an actor its REAL intended sprite name (`drawings/paddle.sprite`) right away and it shows the placeholder until that file is created. Reference real names from the start; don't wait for the art.
|
|
148
150
|
- **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.
|
|
149
151
|
- **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:
|
|
150
152
|
|
|
@@ -158,6 +160,24 @@ Template `components` keys are behavior names (the `static behaviorName`); add a
|
|
|
158
160
|
```
|
|
159
161
|
|
|
160
162
|
- **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).
|
|
163
|
+
- **Sound** — `{ file: "assets/hit.mp3", volume?: 1, playbackRate?: 1, pan?: 0, loop?: false, polyphonic?: false, playOnStart?: true }`. Plays an audio file (`.mp3`/`.wav`/`.m4a`) from an actor. `playbackRate` retunes it (2 = an octave up and twice as fast, 0.5 = an octave down); `pan` places it left (−1) to right (+1); `polyphonic` overlaps copies instead of cutting the last one off, which is what a sound effect fired twice in a row should do (leave it off for music). With `playOnStart` it starts with the scene; otherwise trigger it from code through the handle it puts on the actor:
|
|
164
|
+
|
|
165
|
+
```jsx
|
|
166
|
+
scene.getActor('sfx').runtime.sound.play(); // restarts from the beginning
|
|
167
|
+
await actor.runtime.sound.play(); // ...and wait for it to finish
|
|
168
|
+
actor.runtime.sound.stop(); // also: pause(), resume(), .playing
|
|
169
|
+
scene.sound.stopAll(); // everything the scene is playing, off
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Sound only plays during play, never in the editor, and everything stops on Stop / a scene change. Browsers refuse audio until the player has touched the page; a sound that starts before that is queued and begins on the first tap or key, so don't work around it.
|
|
173
|
+
- **Tone** — `{ note?: 60, waveform?: 'square'|'sawtooth'|'sine'|'triangle'|'noise', attack?: 0, release?: 0.3, volume?: 0.5, pan?: 0, playOnStart?: false }`. Plays a synthesized note — no audio file, nothing downloaded, nothing stored. Reach for this before an audio file for blips, thuds and pickups. `note` is MIDI (60 = middle C, +12 an octave, +1 a semitone), and the note lasts `attack + release` seconds.
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
actor.runtime.tone.play(); // the note as configured
|
|
177
|
+
actor.runtime.tone.play({ note: 72 }); // ...or override per play, e.g. a rising scale
|
|
178
|
+
await actor.runtime.tone.play({ note: 48, waveform: 'noise' });
|
|
179
|
+
```
|
|
180
|
+
- **Video** — `{ file: "assets/clip.mp4", mode?: 'cover'|'fit'|'stretch', playing?: true, loop?: true, muted?: true, volume?: 1 }`. Plays an `.mp4` (H.264) video inside the Layout box, drawn into the scene like a sprite: it moves with Layout, draws in `z` order, and can carry a Collider. `mode` frames it exactly as Sprite's does. In the editor it holds its first frame so you can place it. Leave `muted: true` unless the player has already interacted — an unmuted autoplaying video is blocked by browsers, a muted one isn't.
|
|
161
181
|
|
|
162
182
|
## Physics
|
|
163
183
|
|
|
@@ -308,7 +328,7 @@ anchors.
|
|
|
308
328
|
prototyping; you see your joints working.
|
|
309
329
|
- `hidden` — nothing drawn in play (the joint is pure mechanics). It still shows
|
|
310
330
|
as a faint line in the *editor* so you don't lose track of it.
|
|
311
|
-
- `sprite` — draw art along the joint. Set `sprite` to a `.
|
|
331
|
+
- `sprite` — draw art along the joint. Set `sprite` to a `.sprite`
|
|
312
332
|
(defaults to a built-in rope), `thickness` (px), and `fit`: `tile` (repeat a
|
|
313
333
|
segment — rope, chain) or `stretch` (one image end-to-end — stick, beam). The
|
|
314
334
|
art rotates to the joint's angle and scales to its live length, so a rope
|
|
@@ -349,31 +369,30 @@ draw(actor, scene, ctx) {
|
|
|
349
369
|
- **A global max-speed clamp (`MAX_SPEED` in matterBridge)** backstops any joint
|
|
350
370
|
blow-up so a body can't be flung off screen — normal motion never reaches it.
|
|
351
371
|
|
|
352
|
-
##
|
|
372
|
+
## Where physics lives in the kit
|
|
353
373
|
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
Physics is a normal part of this kit, not a bolt-on module. It uses the same
|
|
375
|
+
directories as everything else, because the rest of the system only looks in
|
|
376
|
+
those places:
|
|
356
377
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
4. In the kit's `editors/behaviorRegistry`, also glob
|
|
368
|
-
`../physics/behaviors/*.jsx` so the physics behaviors auto-register.
|
|
378
|
+
- `behaviors/` — `RigidBody`, `Joints`, `Draggable`, `Slingshot`, `AnalogStick`
|
|
379
|
+
sit alongside `Layout`/`Sprite`/`Collider`/`Camera`. This is the ONE directory
|
|
380
|
+
behaviors live in: the deck file map (`engine/files.js`), the editor's visible
|
|
381
|
+
paths (`castle.json`), the lint config, and the duplicate gate all key off it,
|
|
382
|
+
so a behavior kept anywhere else is invisible to all of them.
|
|
383
|
+
- `engine/physics/` — the simulation itself (`PhysicsSystem.js`, `matterBridge.js`,
|
|
384
|
+
`joints.js`, `jointArt.js`, `controls.js`). Internal, like the rest of `engine/`.
|
|
385
|
+
- `systems/physics.js` — the installer, auto-discovered by
|
|
386
|
+
`engine/systemRegistry.js` and invoked by `makeScene`, so the shared engine
|
|
387
|
+
never references physics directly.
|
|
369
388
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
389
|
+
That last seam is the generic one: any kit can add a per-frame system by dropping
|
|
390
|
+
a file in `systems/` that exports `installSystem(runtime)`, with no engine edits.
|
|
391
|
+
Physics is just this kit's use of it.
|
|
373
392
|
|
|
374
|
-
## Creating pixel art (`.
|
|
393
|
+
## Creating pixel art (`.sprite`)
|
|
375
394
|
|
|
376
|
-
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>.
|
|
395
|
+
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>.sprite`.
|
|
377
396
|
|
|
378
397
|
- **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.
|
|
379
398
|
- **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:
|
|
@@ -387,10 +406,22 @@ Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a
|
|
|
387
406
|
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
|
|
388
407
|
```
|
|
389
408
|
|
|
390
|
-
writes `drawings/ship.
|
|
391
|
-
- **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 `.
|
|
409
|
+
writes `drawings/ship.sprite`. You can also pass `--from file.svg` instead of stdin. An undecodable svg (no usable rects) errors with a nonzero exit and writes nothing.
|
|
410
|
+
- **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 `.sprite` is still playable).
|
|
411
|
+
|
|
412
|
+
Point a `Sprite` component's `file` at the generated `drawings/<name>.sprite` to put the art on an actor.
|
|
413
|
+
|
|
414
|
+
Older decks may still use the `.pxart` extension for the same format; the editor opens and saves both, but **create new art as `.sprite`**.
|
|
415
|
+
|
|
416
|
+
## Path layers (vector source in `.sprite`)
|
|
417
|
+
|
|
418
|
+
Layers come in two kinds. A **pixel** layer is the grid of palette keys described above. A **path** layer also carries vector source: an ordered list of shapes, each a fill plus an optional stroke over subpaths of cubic segments. Shapes composite in painter's order, so a later shape overwrites an earlier one's pixels, and a shape's own multi-subpath fill resolves even-odd (which is how holes are punched).
|
|
419
|
+
|
|
420
|
+
Only **closed** subpaths bound a fill; an open one only strokes. That is how a shape draws its internal divisions — one outline around the silhouette plus an open subpath per division — rather than as two abutting shapes, which would carry two fill keys to keep in step and a seam where each strokes its own side of the shared border. A division that lies exactly on the outline is redundant, and the editor drops it on the next edit, so draw one only where it departs from the outline.
|
|
421
|
+
|
|
422
|
+
The editor always bakes a path layer's shapes down into the cell's `grid`, so **pixels stay the truth** for the runtime and for anything that doesn't know about paths. Path layers are fully editable in the app — pen, select, bend, punch, flip/rotate, z-order, recolor — and pixel layers behave exactly as they did. Converting a layer's kind is lossy in both directions: Pixel → Path throws away the pixels, Path → Pixel throws away the vector source and keeps the baked grid.
|
|
392
423
|
|
|
393
|
-
|
|
424
|
+
To generate path art, emit an svg of **shapes** rather than per-pixel rects and pipe it to `node scripts/import-svg.mjs <name>`; `scripts/svg-emission-guide.md` covers what the importer accepts. `docs/pxart-format.md` is normative for the on-disk form, the half-cell lattice path points are authored onto (guidance for placement, not a storage rule — coordinates are plain floats), the bake, and the sprite's finish fields.
|
|
394
425
|
|
|
395
426
|
## SceneRuntime API (what `scene` exposes to behaviors)
|
|
396
427
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Panel, SelectField } from '
|
|
3
|
-
import { AutoFields, overrideProps } from '
|
|
4
|
-
import { analogVelocity, inActorWorldSpace, pressOnDraggable, stickVector } from '../controls';
|
|
2
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
3
|
+
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
4
|
+
import { analogVelocity, inActorWorldSpace, pressOnDraggable, stickVector } from '../engine/physics/controls';
|
|
5
5
|
|
|
6
6
|
// AnalogStick: an on-screen virtual joystick that drives the actor it's on --
|
|
7
7
|
// the look and feel of castle-client's Analog Stick. Press anywhere to place
|
|
@@ -88,7 +88,7 @@ export class Collider {
|
|
|
88
88
|
// zone for pickup/goal logic) but doesn't block -- collisions are data.
|
|
89
89
|
isTrigger: false,
|
|
90
90
|
debug: false,
|
|
91
|
-
// Physics material, read by physics/matterBridge.js. `bounciness` =
|
|
91
|
+
// Physics material, read by engine/physics/matterBridge.js. `bounciness` =
|
|
92
92
|
// restitution (0 dead .. ~1 very bouncy); `friction` = surface friction;
|
|
93
93
|
// `density` sets mass (= density x total shape area); matter's default 0.001.
|
|
94
94
|
bounciness: 0,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Panel } from '
|
|
3
|
-
import { AutoFields } from '
|
|
4
|
-
import { centerOf, chaseVelocity, inActorWorldSpace } from '../controls';
|
|
2
|
+
import { Panel } from '../engine/ui';
|
|
3
|
+
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
+
import { centerOf, chaseVelocity, inActorWorldSpace } from '../engine/physics/controls';
|
|
5
5
|
|
|
6
6
|
// Draggable: grab the actor with the pointer and fling it around. Chases the
|
|
7
7
|
// pointer with a springy velocity (via the physics system) so it still collides
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { Panel, SelectField, NumberField, CheckboxField, Button } from '
|
|
3
|
-
import { centerOf, inActorWorldSpace } from '../controls';
|
|
4
|
-
import { JOINT_TYPES } from '../joints';
|
|
5
|
-
import { drawJointArt } from '../jointArt';
|
|
2
|
+
import { Panel, SelectField, NumberField, CheckboxField, Button } from '../engine/ui';
|
|
3
|
+
import { centerOf, inActorWorldSpace } from '../engine/physics/controls';
|
|
4
|
+
import { JOINT_TYPES } from '../engine/physics/joints';
|
|
5
|
+
import { drawJointArt } from '../engine/physics/jointArt';
|
|
6
6
|
|
|
7
7
|
const RENDER_MODES = ['line', 'hidden', 'sprite'];
|
|
8
8
|
// Bound anchor offsets (px, local frame). A large offset lengthens the pivot's
|
|
@@ -72,7 +72,7 @@ export class Joints {
|
|
|
72
72
|
|
|
73
73
|
static Inspector({ component, setComponent, override, beginPick, pickActive, files }) {
|
|
74
74
|
const list = Array.isArray(component.list) ? component.list : [];
|
|
75
|
-
const spriteFiles = Object.keys(files ?? {}).filter((f) => f.endsWith('.pxart'));
|
|
75
|
+
const spriteFiles = Object.keys(files ?? {}).filter((f) => f.endsWith('.sprite') || f.endsWith('.pxart'));
|
|
76
76
|
// Which entry's target-pick is live. Combined with the editor's global
|
|
77
77
|
// `pickActive` so a cancel (Esc/empty click) clears every entry's state.
|
|
78
78
|
const [pickingIndex, setPickingIndex] = useState(null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Panel, SelectField } from '
|
|
3
|
-
import { AutoFields, overrideProps } from '
|
|
2
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
3
|
+
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
4
4
|
|
|
5
5
|
// RigidBody makes an actor a physics object. Pair it with a Collider (which
|
|
6
6
|
// gives the shape + material). The simulation reads these props each frame; the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Panel } from '
|
|
3
|
-
import { AutoFields } from '
|
|
4
|
-
import { inActorWorldSpace, pressOnDraggable, slingLaunch, slingPull } from '../controls';
|
|
2
|
+
import { Panel } from '../engine/ui';
|
|
3
|
+
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
+
import { inActorWorldSpace, pressOnDraggable, slingLaunch, slingPull } from '../engine/physics/controls';
|
|
5
5
|
|
|
6
6
|
// Slingshot: pull back and release to fling the actor the opposite way
|
|
7
7
|
// (angry-birds style) -- the physics feel of castle-client's Sling. While
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { resolveDeckFile } from 'castle-web-sdk';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { mediaElement, playMedia, playOneShot, setMediaPan, stopMedia } from '../engine/media';
|
|
4
|
+
import { mediaFilesOfKind } from '../engine/files';
|
|
5
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
6
|
+
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
7
|
+
|
|
8
|
+
// Plays a sound file (.mp3 / .wav / .m4a) from an actor.
|
|
9
|
+
//
|
|
10
|
+
// Two ways to use it. Set `playOnStart` and it plays when the scene starts --
|
|
11
|
+
// music, ambience, a jingle on a game-over screen. Or leave it off and trigger
|
|
12
|
+
// it from a behavior, through the handle this puts on the actor:
|
|
13
|
+
//
|
|
14
|
+
// scene.getActor('sfx').runtime.sound.play(); // from the top
|
|
15
|
+
// await actor.runtime.sound.play(); // ...and wait for it to end
|
|
16
|
+
// actor.runtime.sound.stop();
|
|
17
|
+
//
|
|
18
|
+
// `play()` on a `polyphonic` sound overlaps copies instead of cutting the last
|
|
19
|
+
// one off, which is what a sound effect fired twice in a row should do. Without
|
|
20
|
+
// it, a second play restarts the one element, which is what music wants.
|
|
21
|
+
//
|
|
22
|
+
// Sound only plays during play, never in the editor: `update` is what wires the
|
|
23
|
+
// element up, and the editor doesn't run it. Pressing Stop silences everything,
|
|
24
|
+
// as does `scene.sound.stopAll()` (see systems/media.js).
|
|
25
|
+
//
|
|
26
|
+
// Browsers refuse to play audio until the player has touched the page. A sound
|
|
27
|
+
// that starts before that isn't lost -- it's queued and starts on the first tap
|
|
28
|
+
// or key (engine/media.js), which for a game is usually the first input anyway.
|
|
29
|
+
export class Sound {
|
|
30
|
+
static behaviorName = 'Sound';
|
|
31
|
+
|
|
32
|
+
static defaultProps = {
|
|
33
|
+
file: '',
|
|
34
|
+
volume: 1,
|
|
35
|
+
playbackRate: 1,
|
|
36
|
+
pan: 0,
|
|
37
|
+
loop: false,
|
|
38
|
+
polyphonic: false,
|
|
39
|
+
playOnStart: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
constructor(props) {
|
|
43
|
+
this.props = props;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
update(actor) {
|
|
47
|
+
if (!actor.runtime || actor.runtime.collected) return;
|
|
48
|
+
const file = resolveDeckFile(this.props.file);
|
|
49
|
+
const element = mediaElement(actor.id, 'audio', file);
|
|
50
|
+
if (!element) {
|
|
51
|
+
actor.runtime.sound = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Props are live: turning the volume down, the pitch up, or the loop on in
|
|
55
|
+
// the inspector takes effect on the sound already playing.
|
|
56
|
+
const settings = readSettings(this.props);
|
|
57
|
+
element.volume = settings.volume;
|
|
58
|
+
element.playbackRate = settings.playbackRate;
|
|
59
|
+
element.loop = settings.loop;
|
|
60
|
+
if (settings.pan !== 0) setMediaPan(element, settings.pan);
|
|
61
|
+
|
|
62
|
+
const handle = actor.runtime.sound;
|
|
63
|
+
if (!handle || handle.file !== file) {
|
|
64
|
+
actor.runtime.sound = makeHandle(file, element, () => readSettings(this.props));
|
|
65
|
+
if (this.props.playOnStart) void actor.runtime.sound.play();
|
|
66
|
+
} else {
|
|
67
|
+
handle.settings = settings;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static Inspector({ component, setComponent, override }) {
|
|
72
|
+
return (
|
|
73
|
+
<Panel title="Sound" overridden={override?.anyOverridden()}>
|
|
74
|
+
<SelectField
|
|
75
|
+
label="File"
|
|
76
|
+
value={component.file}
|
|
77
|
+
onChange={(file) => setComponent({ file })}
|
|
78
|
+
options={mediaFilesOfKind('audio')}
|
|
79
|
+
{...overrideProps(override, 'file')}
|
|
80
|
+
/>
|
|
81
|
+
<AutoFields
|
|
82
|
+
defaultProps={Sound.defaultProps}
|
|
83
|
+
component={component}
|
|
84
|
+
setComponent={setComponent}
|
|
85
|
+
only={['volume', 'playbackRate', 'pan', 'loop', 'polyphonic', 'playOnStart']}
|
|
86
|
+
override={override}
|
|
87
|
+
/>
|
|
88
|
+
</Panel>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clamp(value, min, max, fallback) {
|
|
94
|
+
return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readSettings(props) {
|
|
98
|
+
return {
|
|
99
|
+
volume: clamp(props.volume, 0, 1, 1),
|
|
100
|
+
// The same ceiling the classic editor used: past 10x a sound is a click.
|
|
101
|
+
playbackRate: clamp(props.playbackRate, 0.01, 10, 1),
|
|
102
|
+
pan: clamp(props.pan, -1, 1, 0),
|
|
103
|
+
loop: Boolean(props.loop),
|
|
104
|
+
polyphonic: Boolean(props.polyphonic),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// What a behavior gets at `actor.runtime.sound`. Plain methods over the element,
|
|
109
|
+
// so game code never has to know an <audio> is involved.
|
|
110
|
+
//
|
|
111
|
+
// `play()` returns a promise that resolves when the sound ends, so a behavior
|
|
112
|
+
// can `await` one before doing the next thing. A looping sound resolves right
|
|
113
|
+
// away rather than never -- awaiting something that by definition doesn't end
|
|
114
|
+
// would hang the caller forever, and that is never what was meant.
|
|
115
|
+
function makeHandle(file, element, currentSettings) {
|
|
116
|
+
const handle = {
|
|
117
|
+
file,
|
|
118
|
+
element,
|
|
119
|
+
settings: currentSettings(),
|
|
120
|
+
play() {
|
|
121
|
+
const s = handle.settings;
|
|
122
|
+
if (s.polyphonic) {
|
|
123
|
+
return playOneShot(file, { volume: s.volume, playbackRate: s.playbackRate, pan: s.pan });
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
element.currentTime = 0;
|
|
127
|
+
} catch {
|
|
128
|
+
// Not seekable yet (still loading) -- it plays from the start anyway.
|
|
129
|
+
}
|
|
130
|
+
playMedia(element);
|
|
131
|
+
if (s.loop) return Promise.resolve();
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
const done = () => resolve();
|
|
134
|
+
element.addEventListener('ended', done, { once: true });
|
|
135
|
+
element.addEventListener('error', done, { once: true });
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
resume() {
|
|
139
|
+
playMedia(element);
|
|
140
|
+
},
|
|
141
|
+
pause() {
|
|
142
|
+
element.pause();
|
|
143
|
+
},
|
|
144
|
+
stop() {
|
|
145
|
+
stopMedia(element);
|
|
146
|
+
},
|
|
147
|
+
get playing() {
|
|
148
|
+
return !element.paused;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return handle;
|
|
152
|
+
}
|