@pix3/cli 1.6.0 → 1.6.3

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 (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-CBFYKDS7.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-CFQFEI52.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +39 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +43 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
@@ -1,15 +1,15 @@
1
- <!-- Pix3 agent kit 1.6.0 — generated from docs/nodes-and-systems.md, src/services/agent/agent-skills/engine-api-map.md and the runtime's component registry. Do not edit: pix3 kit --update regenerates it. -->
2
- # Script API reference
3
-
4
- Generated from the engine's own documentation and code. Grep a heading instead of reading it
5
- whole. Tool names the in-editor agent uses appear in places: `add_component` /
6
- `set_component_property` = edit the scene YAML's `components:`; `list_component_types` = the
7
- "`core:` components" section at the end of this file; `engine_search` / `engine_read` do not
8
- exist for you — the runtime's declarations are in `.pix3/types/@pix3/runtime/` (or in
9
- `node_modules/@pix3/runtime` when the project has its own `tsconfig.json`). Source paths quoted here (`src/…`, `packages/…`) are files of the Pix3 engine repository, not of this project.
10
-
11
- ## The surface a `Script` sees
12
-
1
+ <!-- Pix3 agent kit 1.6.3 — generated from docs/nodes-and-systems.md, src/services/agent/agent-skills/engine-api-map.md and the runtime's component registry. Do not edit: pix3 kit --update regenerates it. -->
2
+ # Script API reference
3
+
4
+ Generated from the engine's own documentation and code. Grep a heading instead of reading it
5
+ whole. Tool names the in-editor agent uses appear in places: `add_component` /
6
+ `set_component_property` = edit the scene YAML's `components:`; `list_component_types` = the
7
+ "`core:` components" section at the end of this file; `engine_search` / `engine_read` do not
8
+ exist for you — the runtime's declarations are in `.pix3/types/@pix3/runtime/` (or in
9
+ `node_modules/@pix3/runtime` when the project has its own `tsconfig.json`). Source paths quoted here (`src/…`, `packages/…`) are files of the Pix3 engine repository, not of this project.
10
+
11
+ ## The surface a `Script` sees
12
+
13
13
  Inside any `Script` subclass:
14
14
 
15
15
  - `this.node` — the owning `NodeBase` (transform, `visible`, `getComponent`, `addComponent`, `connect`/`emit`, `findById`/`findByName`/`findByPath`, `children`, `parentNode`). `getComponent<T>(type: new (...args) => T): T | null` takes the component **class**, not a string ID — `node.getComponent(CarController)`, importing the class by relative path (`./CarController`). There is no string-based lookup (`getComponent('user:CarController')` fails); `user:*` IDs are for `add_component`/scene YAML only. To fetch by hand: `node.components.find(c => c instanceof CarController)`.
@@ -49,12 +49,12 @@ cached texture (the runtime crops a private clone that shares the GPU image), so
49
49
  you never clone textures yourself. Author the region once and drive it from
50
50
  **both** `onUpdate` (play) and `tickEditorPreview` (edit) so the two modes match. A
51
51
  throwing `tickEditorPreview` disables the component and surfaces the error like a
52
- play-mode hook, so the editor keeps running.
53
-
54
- ## Engine API map
55
-
56
- ### A `Script`
57
-
52
+ play-mode hook, so the editor keeps running.
53
+
54
+ ## Engine API map
55
+
56
+ ### A `Script`
57
+
58
58
  - Lifecycle: `onAttach(node)` → `onStart()` (first frame) → `onUpdate(dt)` (scaled seconds;
59
59
  frozen by hitstop) → `onDetach()`. Defaults in the constructor's `this.config = {…}`;
60
60
  `static getPropertySchema()` exposes them to the inspector / `set_component_property`.
@@ -62,10 +62,10 @@ play-mode hook, so the editor keeps running.
62
62
  editor previews), `this.input` (`InputService`), `this.findNode(q)` (id / name / path →
63
63
  node or null), `this.getNode(q)` (throws when missing).
64
64
  - Reach another script: `import { Other } from './Other'; this.node.getComponent(Other)` —
65
- the class, never the `user:Other` string.
66
-
67
- ### Nodes
68
-
65
+ the class, never the `user:Other` string.
66
+
67
+ ### Nodes
68
+
69
69
  - Transform is three.js and **read-only by reference**: `node.position.set(x, y, 0)`,
70
70
  `node.position.x += dx`, `node.rotation.z = radians`, `node.scale.set(s, s, 1)`. Never
71
71
  assign `node.position = …`.
@@ -99,10 +99,10 @@ play-mode hook, so the editor keeps running.
99
99
  `Hemisphere`, `Directional`, `Point`, `Spot`), `PostProcess` (bloom / vignette /
100
100
  chromatic aberration; `affect2D: true` for 2D scenes), `AudioPlayer`.
101
101
  - Shader effects on a 2D node: `effects: [{ type: 'core:tint', params: { color, amount } }]`
102
- (also `core:adjust`, `core:grayscale`) — the way a near-white placeholder takes a colour.
103
-
104
- ### `this.scene`
105
-
102
+ (also `core:adjust`, `core:grayscale`) — the way a near-white placeholder takes a colour.
103
+
104
+ ### `this.scene`
105
+
106
106
  - Lookup: `findNode(q)`, `findNodeById`, `findNodeByName`, `findNodeByPath`, `getRootNodes()`,
107
107
  `getActiveCamera()`, `getActiveCamera2D()`, `getViewportInfo()`, `isPortrait()`.
108
108
  - **Pointer in 2D world space:** `scene.getPointer2DWorldPosition()` → `Vector2 | null`
@@ -171,12 +171,15 @@ play-mode hook, so the editor keeps running.
171
171
  - Cutscenes: `scene.cutscene.playCinematic(id, {…})`; camera blends via `core:CameraBrain`
172
172
  + `VirtualCamera3D`.
173
173
  - Multiplayer: `scene.network`, `scene.netNodes`, `core:NetworkedNode`,
174
- `core:ReplicatedTransform` (only when the brief says multiplayer).
175
-
176
- ### `this.input`
177
-
174
+ `core:ReplicatedTransform` (only when the brief says multiplayer).
175
+
176
+ ### `this.input`
177
+
178
178
  - Per frame: `input.pointerEvents` → `{ type: 'down' | 'move' | 'up' | 'cancel', pointerId,
179
- x, y }[]` (`cancel` is never a completed tap); `input.keyEvents`; `input.wheelDelta`.
179
+ x, y }[]` (`cancel` is never a completed tap); `input.keyEvents`; `input.wheelDelta` (plain
180
+ scroll wheel only); `input.wheelZoomDelta` (Ctrl/⌘ + wheel and trackpad pinch — use it for
181
+ camera zoom, it is excluded from `wheelDelta`); `input.wheelModifiers` (`{ ctrl, meta, shift,
182
+ alt }`). The canvas already `preventDefault()`s every wheel event, so ctrl-wheel never zooms the page.
180
183
  - Polled: `input.isPointerDown` (any finger), `input.pointerPosition` (primary finger,
181
184
  canvas space — convert with `scene.getPointer2DWorldPosition()`), `input.getActivePointers()`,
182
185
  `input.getPointer(id)`, `input.pointerDownCount`, `input.isPointerOverUI(pointerId)` (gate
@@ -185,19 +188,19 @@ play-mode hook, so the editor keeps running.
185
188
  - Keyboard: compare `event.code` (`'KeyW'`, `'ArrowUp'`), case-sensitive.
186
189
  - A tap-to-place mechanic is: on a `'down'` event, `scene.getPointer2DWorldPosition(ev.pointerId)`,
187
190
  convert to the board's local space (`board.worldToLocal` or subtract the board's world
188
- position), place the node, `scene.juice.burst({ x, y })`, `scene.audio.sfx('tap')`.
189
-
190
- ### Traps
191
-
191
+ position), place the node, `scene.juice.burst({ x, y })`, `scene.audio.sfx('tap')`.
192
+
193
+ ### Traps
194
+
192
195
  - Assigning `position` / `rotation` / `scale` throws — mutate them.
193
196
  - A component that throws in `onStart` / `onUpdate` is auto-disabled and the game keeps
194
197
  running — `read_errors` after `play_start` is the only way to see it.
195
198
  - `this.node as any` disables exactly the check that would have caught the above.
196
199
  - Hitstop every frame while an overlap lasts freezes the contact forever — edge-trigger it.
197
- - A hidden `UIControl2D` takes no input; `tick` still runs on hidden nodes.
198
-
199
- ## Capability catalog — nodes
200
-
200
+ - A hidden `UIControl2D` takes no input; `tick` still runs on hidden nodes.
201
+
202
+ ## Capability catalog — nodes
203
+
201
204
  Add via the editor **Create** menu / `Create*Command`, or author in `.pix3scene`
202
205
  YAML, or (from a script) construct + `parent.adoptChild(child)`. Full property
203
206
  tables: node-types-reference.md.
@@ -215,7 +218,7 @@ tables: node-types-reference.md.
215
218
  **Skins:** these controls are colour-driven by default and take sprites through texture slots — `Button2D` `textureNormal/Hover/Pressed/Disabled`, `Checkbox2D` `textureBox` / `textureBoxChecked` / `textureMark`, `Slider2D` `textureTrack` / `textureFill` / `textureThumb`, `Bar2D` `textureTrough` / `textureFill`, plus `ScrollContainer2D`'s thumb/track. A set slot replaces that flat colour; an unset one keeps it. `Button2D`, `Slider2D` (track+fill) and `Bar2D` (trough+fill) also take the four `sliceBorder*` nine-slice insets `TiledSprite2D` uses, so one 64x64 skin fits any size instead of smearing — and a fill that shrinks with `value` is re-cut, not squashed. A sliced skin opts out of the 2D quad batcher.
216
219
  `Label2D` is multiline: a fixed `width` word-wraps, `labelAlign`/`labelVAlign` align inside the box, and `typewriterSpeed` + `setText()`/`skipTypewriter()`/`'typewriter-complete'` give a per-character reveal.
217
220
  - `Camera2D` — pan/zoom/limits/shake for the 2D pass. `CanvasLayer2D` — fixed HUD layer, unaffected by Camera2D.
218
- - `AnimatedSprite2D` (and `AnimatedSprite3D`) play a flipbook from a **`.pix3anim`** resource — see the recipe below. A non-looping clip emits **`animation-finished`** (clip name as arg) when it stops on the last frame. For self-freeing one-shot VFX set **`freeOnFinish: true`** on the node (destroys itself when the clip ends — no component); use `core:FreeOnSignal` only when the trigger is some _other_ signal.
221
+ - `AnimatedSprite2D` (and `AnimatedSprite3D`) play a flipbook from a **`.pix3anim`** resource — see the recipe below. Switch clips from a script with **`sprite.play('attack', { restart: true })`** (returns `false` for an unknown clip; `restart` replays the current clip from frame 0, which is how a finished one-shot is fired again — writing `currentClip` alone never restarts). A non-looping clip emits **`animation-finished`** (clip name as arg) when it stops on the last frame. For self-freeing one-shot VFX set **`freeOnFinish: true`** on the node (destroys itself when the clip ends — no component); use `core:FreeOnSignal` only when the trigger is some _other_ signal.
219
222
 
220
223
  **Flipbook animation (`.pix3anim`)** — hand-author it; the file is plain JSON and `SceneLoader` auto-loads the resource + every frame texture (also when the node arrives via `scene.instantiate` of a prefab). Every omitted field is defaulted on load (fps 12, loop true, `playbackMode` normal, anchor 0.5/0.5, `durationMultiplier` 1). Save it next to the frames, point the node at it. **Sequence mode** (one image per frame — the common case, e.g. an impact flash):
221
224
 
@@ -238,6 +241,8 @@ tables: node-types-reference.md.
238
241
  }
239
242
  ```
240
243
 
244
+ **Character with variants/states (weapon skins, outfits)** — one `.pix3anim` whose clips are named `<variant>.<state>` (`sword.idle`, `sword.attack`, `bow.idle`, …; a clip without the separator is a variant-less state such as `die`), an `AnimatedSprite2D` as the prefab root (`sizeMode: native` — every frame carries `sourceSize`; the frame `anchor` at the feet, so the node's position is where the character stands) and **`core:CharacterVisual2D`** on that same node (`variant`, `state`, `separator`, optional `spriteNodeId`; empty = the host itself, else its first `AnimatedSprite2D` child). Keep the sprite as the root: selecting the character then selects the drawn quad, and the viewport frames the visible art and marks the pivot at the node position — a `Group2D` wrapper frames a centred box that the frame anchor pushes off the art. Game code says `character.playState('attack', { restart: true })` and `character.setVariant('bow')` (keeps the state, restarts its clip) and never spells a clip name; both return `false` for a pair the resource lacks, no hidden fallback. A finished one-shot holds its last frame and the root emits **`state-finished`** `(state, variant)` — the component adds no movement, physics, AI or automatic transitions. `getVariants()` / `getStates(variant?)` read the vocabulary off the clip names. This is Unity's Sprite Library categories/labels + Sprite Resolver, not an AnimationTree. `compileCharacter` (`src/services/library/character-compiler.ts`) emits exactly this shape from grouped frame files — the Store's `character2d` asset kind — as a managed sprite folder (`sprites/<slug>/<slug>.pix3anim` + `<variant>_<state>_<nnnn>.png`) plus `prefabs/<Name>.pix3scene`; pass `anchor` (y from the top) at the feet row so the node's position is where the character stands — the default is the canvas centre.
245
+
241
246
  **Spritesheet mode** instead: set top-level `texturePath` and give each frame a UV rect `offset:{x,y}` + `repeat:{x,y}` (these default to 0 → sample nothing, so they're required here). Frames may carry `durationMultiplier` and `events:[{signal,args}]` (fired on play-driven frame entry). Node wiring: `type: AnimatedSprite2D`, properties `animationResourcePath`, `currentClip`, `isPlaying`, `freeOnFinish` (one-shot self-destruct), `width`/`height`, `anchor`, `sizeMode`. First spawn of a runtime-instantiated clip warms its texture cache; if the first play must be pixel-perfect, spawn one invisible warm-up at level start. Authoring GUI: the editor's **Sprite Editor** produces the same file — one shell (canvas + clips rail + frame timeline) that edits both a bare image and a `.pix3anim`; selecting a frame binds the canvas to that frame's texture, and crop / rotate / flip / background-removal / generation write straight back into the frame.
242
247
 
243
248
  **Frame presentation — `sizeMode`, per-frame `anchor`, `sourceSize`.** Two anchors are in play and they mean different things. The **node** `anchor` is a global pivot in the node's `width × height` box (y up, same as `Sprite2D.anchor`). Each **frame's** `anchor` is that frame's own origin inside its — possibly tightly cropped — raster, normalized with **y measured from the top** (image convention, like `boundingBox`). They compose: the quad is placed so the frame anchor lands on the node's position, then shifted by the node pivot. That is what makes cropping pay off — crop a frame tighter, move its anchor to the old visual centre, and the animation is pixel-identical while the PNG (and the atlas) shrinks. `sizeMode` decides how a frame fills the box: `'stretch'` (default, and what every pre-existing scene assumes) scales every frame to exactly `width × height`; `'native'` renders each frame at its own `sourceSize` scaled by one per-clip factor derived from the clip's FIRST frame, so mixed-size frames keep their relative proportions and resizing the node scales the whole animation uniformly (the editor sets `'native'` on newly created nodes). `sourceSize` is an optional per-frame `{width,height}` the editor stamps whenever a frame is added, imported or sliced, so native layout never waits on a texture load; a frame with no known size falls back to stretch, so legacy files keep working. The math lives in one shared module (`core/animated-sprite-layout.ts`) because the editor viewport draws SEPARATE proxy meshes — both apply the same resolver.
@@ -290,10 +295,10 @@ cost is per skeleton per frame, so budget dozens, not hundreds. Full property ta
290
295
  **Other**
291
296
 
292
297
  - `AudioPlayer` — a scene-graph audio source (§4 Audio).
293
- - `PostProcess` — enables the post-processing pipeline (§4 Post-processing).
294
-
295
- ## Capability catalog — `core:*` behaviours
296
-
298
+ - `PostProcess` — enables the post-processing pipeline (§4 Post-processing).
299
+
300
+ ## Capability catalog — `core:*` behaviours
301
+
297
302
  Attach in the inspector or in YAML `components:`. These are the pre-built,
298
303
  designer-facing behaviors — prefer them over writing a script for the same
299
304
  effect. Registered in
@@ -310,6 +315,7 @@ packages/pix3-runtime/src/behaviors/register-behaviors.ts.
310
315
  | `core:RadialProgress` | Circular progress mask on a Sprite2D |
311
316
  | `core:AnimationPlayer` | Play keyframe clips on this node + descendants (§4) |
312
317
  | `core:PointAttachment` | Keep this node on a named frame point of a parent `AnimatedSprite2D` (hand socket, muzzle) every tick, optionally copying the point's angle |
318
+ | `core:CharacterVisual2D` | `variant + state → clip` on an `AnimatedSprite2D` with clips named `<variant>.<state>`; `playState`/`setVariant`, `state-finished` |
313
319
  | `core:PlaySound` | Play a sound when a node signal fires |
314
320
  | `core:SfxOnSignal` | Play a **procedural** (asset-free) sound preset when a node signal fires — see §4 "Procedural SFX" |
315
321
  | `core:BurstOnSignal` | Spawn a one-shot 2D particle burst at this node when a signal fires (juice) |
@@ -335,13 +341,13 @@ so a keyframe **event track** or a script `emit()` can fire them.
335
341
  (3D-only), `core:uv-scroll`, `core:flash`, `core:adjust`
336
342
  (brightness/contrast/saturation), `core:grayscale`, `core:tint`. Params are
337
343
  keyframe-animatable. See
338
- packages/pix3-runtime/src/shader-effects/.
339
-
340
- ## `core:` components — every config key
341
-
342
- Read from the runtime's component registry for this CLI's version: the keys a `config:` block
343
- may carry, their type, the default a fresh component has, and the inspector's range/options.
344
-
344
+ packages/pix3-runtime/src/shader-effects/.
345
+
346
+ ## `core:` components — every config key
347
+
348
+ Read from the runtime's component registry for this CLI's version: the keys a `config:` block
349
+ may carry, their type, the default a fresh component has, and the inspector's range/options.
350
+
345
351
  ### `core:AnimationPlayer`
346
352
 
347
353
  Plays keyframe animation clips on this node and its descendants
@@ -373,6 +379,17 @@ Blends the render camera between virtual cameras by priority (Cinemachine-lite)
373
379
  | --- | --- | --- | --- |
374
380
  | `blendEnabled` | boolean | `true` | Ease between cameras on activation. Off = hard cut. |
375
381
 
382
+ ### `core:CharacterVisual2D`
383
+
384
+ Plays variant + state as a clip of an AnimatedSprite2D whose clips are named <variant>.<state> (sword.idle); swap the variant, keep the state vocabulary
385
+
386
+ | Key | Type | Default | Notes |
387
+ | --- | --- | --- | --- |
388
+ | `variant` | string | `""` | Clip-name prefix (sword, bow). Empty when clips are named by state alone |
389
+ | `state` | string | `""` | State played on start (idle) |
390
+ | `separator` | string | `"."` | Text between variant and state in clip names |
391
+ | `spriteNodeId` | string | `""` | AnimatedSprite2D to drive; empty = this node or its first sprite child |
392
+
376
393
  ### `core:Collider2D`
377
394
 
378
395
  Physics shape (rect/circle/polygon, rotation-aware, may be concave). With no Physics Body above it this is static world geometry; as a sensor it reports body-entered / body-exited.
@@ -635,4 +652,4 @@ Oscillates a node along a selected axis
635
652
  | --- | --- | --- | --- |
636
653
  | `amplitude` | number | `1` | ≥ 0; Maximum displacement from the center |
637
654
  | `frequency` | number | `1` | ≥ 0; Oscillations per second |
638
- | `axis` | select | `"y"` | x \| y \| z; The axis to oscillate along |
655
+ | `axis` | select | `"y"` | x \| y \| z; The axis to oscillate along |