@pix3/cli 1.6.4 → 2.0.0-alpha.0

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 (168) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +722 -788
  3. package/dist/index.js +557 -421
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
  6. package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
  7. package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
  8. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  9. package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
  10. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  11. package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
  12. package/dist/validate/prebuilt/bundle-main.js +223 -18
  13. package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
  14. package/dist/validate/prebuilt/runtime.js +1 -1
  15. package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
  16. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
  17. package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
  18. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
  19. package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
  20. package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
  21. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
  22. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
  23. package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
  24. package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
  25. package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
  26. package/kit/files/AGENTS.md +76 -84
  27. package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
  28. package/kit/kit.json +8 -5
  29. package/package.json +62 -54
  30. package/templates/2d/files/scenes/main.pix3scene +31 -0
  31. package/templates/2d/template.yaml +11 -0
  32. package/templates/3d/files/scenes/main.pix3scene +50 -0
  33. package/templates/3d/template.yaml +11 -0
  34. package/templates/base/files/README.md +39 -0
  35. package/templates/base/files/gitignore +11 -0
  36. package/templates/base/files/index.html +23 -0
  37. package/templates/base/files/package.json +28 -0
  38. package/templates/base/files/src/main.ts +3 -0
  39. package/templates/base/files/tsconfig.json +17 -0
  40. package/templates/base/files/vite.config.ts +10 -0
  41. package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
  42. package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
  43. package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
  44. package/templates/empty-2d/files/README.md +0 -20
  45. package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
  46. package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
  47. package/templates/empty-2d/template.yaml +0 -7
  48. package/templates/empty-3d/files/README.md +0 -20
  49. package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
  50. package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
  51. package/templates/empty-3d/template.yaml +0 -7
  52. package/templates/idea-blank/files/README.md +0 -22
  53. package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
  54. package/templates/idea-blank/template.yaml +0 -10
  55. package/templates/minigame-2d/files/README.md +0 -44
  56. package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
  57. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
  58. package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
  59. package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
  60. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
  61. package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
  62. package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
  63. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
  64. package/templates/minigame-2d/template.yaml +0 -10
  65. package/templates/playable-2d/files/README.md +0 -30
  66. package/templates/playable-2d/files/design/recipe.md +0 -83
  67. package/templates/playable-2d/files/design/tests/reachability.json +0 -4
  68. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
  69. package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
  70. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
  71. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
  72. package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
  73. package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
  74. package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
  75. package/templates/playable-2d/template.yaml +0 -9
  76. package/templates/playable-3d/files/README.md +0 -32
  77. package/templates/playable-3d/files/design/recipe.md +0 -127
  78. package/templates/playable-3d/files/design/tests/reachability.json +0 -4
  79. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
  80. package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
  81. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
  82. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
  83. package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
  84. package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
  85. package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
  86. package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
  87. package/templates/playable-3d/template.yaml +0 -9
  88. package/templates/recipe-arena-2d/files/README.md +0 -48
  89. package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
  90. package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
  91. package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
  92. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
  93. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
  94. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
  95. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
  96. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
  97. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
  98. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
  99. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
  100. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
  101. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
  102. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
  103. package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
  104. package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
  105. package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
  106. package/templates/recipe-arena-2d/template.yaml +0 -10
  107. package/templates/recipe-blank-2d/files/README.md +0 -61
  108. package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
  109. package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
  110. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
  111. package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
  112. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
  113. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
  114. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
  115. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
  116. package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
  117. package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
  118. package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
  119. package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
  120. package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
  121. package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
  122. package/templates/recipe-blank-2d/template.yaml +0 -10
  123. package/templates/recipe-bouncer-2d/files/README.md +0 -81
  124. package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
  125. package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
  126. package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
  127. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
  128. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
  129. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
  130. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
  131. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
  132. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
  133. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
  134. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
  135. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
  136. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
  137. package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
  138. package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
  139. package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
  140. package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
  141. package/templates/recipe-bouncer-2d/template.yaml +0 -10
  142. package/templates/recipe-grid-3d/files/README.md +0 -22
  143. package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
  144. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
  145. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
  146. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
  147. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
  148. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
  149. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
  150. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
  151. package/templates/recipe-grid-3d/template.yaml +0 -10
  152. package/templates/recipe-tapper-2d/files/README.md +0 -39
  153. package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
  154. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
  155. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
  156. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
  157. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
  158. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
  159. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
  160. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
  161. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
  162. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
  163. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
  164. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
  165. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
  166. package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
  167. package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
  168. package/templates/recipe-tapper-2d/template.yaml +0 -10
@@ -1,4 +1,4 @@
1
- <!-- Pix3 agent kit 1.6.4 — generated from docs/pix3-specification.md ("Scene File Format"). Do not edit: pix3 kit --update regenerates it. -->
1
+ <!-- Pix3 agent kit 2.0.0-alpha.0 — generated from docs/pix3-specification.md ("Scene File Format"). Do not edit: pix3 kit --update regenerates it. -->
2
2
  # `.pix3scene` — the engine specification
3
3
 
4
4
  Excerpts of the engine's specification, as shipped with this CLI. `SKILL.md` beside this file
@@ -11,14 +11,14 @@ layout (`res://` is always relative to the project root). Source paths quoted he
11
11
 
12
12
  - Declarative: The file describes the composition and structure of the scene, not the process of its creation.
13
13
  - Asset Referencing: Assets (models, textures) are not embedded in the file but are referenced via relative paths with a res:// prefix (path from the project root).
14
- - Flat Asset Layout: a game project keeps one folder per asset type **at its root** — `scenes/`, `sprites/` (images/textures), `models/`, `audio/`, `fonts/`, `spine/` (skeleton + atlas + pages together), `scripts/`, `locales/` — with free subdivision inside (`sprites/ui/…`). There is no `assets/` wrapper folder; the project templates ship this layout and `defaultAssetFolder()` (`src/core/asset-categories.ts`) is the canonical mapping used when the editor creates an asset from a bare file name.
15
- - SVG textures: an `.svg` works as a `Sprite2D` / `Button2D` texture only when its root `<svg>` carries `xmlns="http://www.w3.org/2000/svg"` and px `width`/`height` (a viewBox alone has no intrinsic size), it is self-contained, and the Blob it is decoded from is typed `image/svg+xml` — every read path therefore types image/audio blobs by extension when the stored type is generic (`CloudProjectCacheService.readBlob`, `PlayableHtmlBuildService.resolveMimeType`). `pix3 validate` checks referenced SVGs (`E_SVG_INVALID`, `E_SVG_NO_SIZE`, `W_SVG_VIEWBOX_ONLY`, `W_SVG_EXTERNAL_REF`).
14
+ - Flat Asset Layout: a game project keeps one folder per asset type **at its root** — `scenes/`, `sprites/` (images/textures), `models/`, `audio/`, `fonts/`, `spine/` (skeleton + atlas + pages together), `scripts/`, `locales/` — with free subdivision inside (`sprites/ui/…`). There is no `assets/` wrapper folder; `pix3 new` creates this layout.
15
+ - SVG textures: an `.svg` works as a `Sprite2D` / `Button2D` texture only when its root `<svg>` carries `xmlns="http://www.w3.org/2000/svg"` and px `width`/`height` (a viewBox alone has no intrinsic size), it is self-contained, and the Blob it is decoded from is typed `image/svg+xml` — the editor types the bytes it reads by extension (`packages/editor-core/src/services/project/file-content-type.ts`) and a build embeds every asset with its type (`packages/vite-plugin/src/files/content-type.ts`). `pix3 validate` checks referenced SVGs (`E_SVG_INVALID`, `E_SVG_NO_SIZE`, `W_SVG_VIEWBOX_ONLY`, `W_SVG_EXTERNAL_REF`).
16
16
  - Composition: Complex scenes are assembled from simpler ones by instantiating other scene files.
17
17
  - Unambiguous Structure: An explicit children key is used to denote the list of child nodes, which separates the hierarchy from the properties of the node itself.
18
18
  - Unique Identification: Every node must have an id field. The value is a short, cryptographically secure unique identifier (similar to Nano ID) to provide a balance between file readability and the absolute reliability of references.
19
19
  - Versioned Schema: Each file includes a `version` field; migrations are maintained in the SceneManager and run automatically on load.
20
20
  - Conflict Resolution: Instance overrides always win over parent definitions. Duplicate IDs trigger validation errors during import.
21
- - Forgiving Vocabulary: a `type:` the loader does not know does **not** fail the load — the node is built as a bare `NodeBase` placeholder that does nothing, so a scene written by a newer version (or with a typo) stays openable and editable. Because such a node is otherwise indistinguishable from a working one, it is reported on three surfaces: the renderability lint (`inert-nodes` in `scene_tree` / `game_observe` / play start), the loader diagnostic (`describeUnknownNodeType`, which suggests the nearest known name), and the Scene Tree row, which carries a warn badge and states the reason in its tooltip. `isInertNode` (`packages/pix3-runtime/src/core/renderability-lint.ts`) is the one predicate behind all three.
21
+ - Forgiving Vocabulary: a `type:` the loader does not know does **not** fail the load — the node is built as a bare `NodeBase` placeholder that does nothing, so a scene written by a newer version (or with a typo) stays openable and editable. Because such a node is otherwise indistinguishable from a working one, it is reported on three surfaces: the renderability lint (`inert-nodes`, a warning when play starts and in `pix3_game_run` observations), the loader diagnostic (`describeUnknownNodeType`, which suggests the nearest known name), and the Scene Tree row, which carries a warn badge and states the reason in its tooltip. `isInertNode` (`packages/runtime/src/core/renderability-lint.ts`) is the one predicate behind all three; `pix3 validate` reports the same node as `E_UNKNOWN_NODE_TYPE`.
22
22
 
23
23
  ## Skinned 2D UI controls
24
24
 
@@ -57,13 +57,9 @@ The four `sliceBorderLeft/Right/Top/Bottom` scalars are the same nine-slice inse
57
57
  fill; the slider thumb is drawn at its authored size and is never sliced. A fill
58
58
  that shrinks with `value` is **re-cut** at the new width, so its end caps keep
59
59
  their pixel size. All-zero (the default) is the plain stretch. A sliced skin opts
60
- out of the 2D quad batcher, because the batcher extracts four *unit* corners, and
60
+ out of the 2D quad batcher, because the batcher extracts four _unit_ corners, and
61
61
  is excluded from the pre-launch texture atlas for the same reason.
62
62
 
63
- Sprites for every one of these slots — and the matching insets — can be generated
64
- and applied without leaving the editor; the files and their manifest are specified
65
- in **§6.23 UI Kit Assets**.
66
-
67
63
  ```yaml
68
64
  - id: 'q7Bd-1mZ_kTn0-Vx4pA'
69
65
  type: 'Button2D'
@@ -114,7 +110,7 @@ in **§6.23 UI Kit Assets**.
114
110
  A `SpineSkeleton2D` references its Spine export by two `res://` paths — the
115
111
  skeleton (`.json` or `.skel`) and the atlas (`.atlas`). The atlas' page images are
116
112
  NOT listed: they are named inside the atlas text and resolved relative to it (the
117
- export asset collector parses `.atlas` files for the same reason). `texture` is an
113
+ build's asset scan, `packages/vite-plugin/src/build/scan.ts`, parses `.atlas` files for the same reason). `texture` is an
118
114
  optional single-page override.
119
115
 
120
116
  ```yaml
@@ -151,14 +147,30 @@ Groups provide runtime categorization for nodes (for example, `enemies`, `ui`, `
151
147
  - `removeFromGroup(group)`
152
148
  - `isInGroup(group)`
153
149
 
154
- #### 6.19.2 Scene API
155
-
156
- `SceneManager` provides group-based queries and invocation:
150
+ A node's groups are its own `groups` set (names trimmed); it is the only membership record —
151
+ nothing indexes it, so a change is seen by the next query.
157
152
 
158
- - `getNodesInGroup(group)` - returns matching nodes in the active scene.
159
- - `callGroup(group, method, ...args)` - calls matching component methods across grouped nodes.
153
+ #### 6.19.2 Scene API
160
154
 
161
- `callGroup` performs runtime method checks and warns if no callable method is found.
155
+ A script queries and calls groups through `this.scene` (`SceneService`), during play and in the
156
+ exported player alike:
157
+
158
+ - `getNodesInGroup(group)` - the running scene's nodes in `group`, in tree order (depth-first,
159
+ a parent before its children, roots in order); empty outside a running game. Read at call
160
+ time from the nodes' `groups`, so `addToGroup` / `removeFromGroup`, a spawned prefab, a freed
161
+ node and `changeScene` all show in the next call. Like `findNode`, it walks the running scene
162
+ only — not the autoloads' hidden root, and not the editor's authored graph (play runs on a
163
+ clone).
164
+ - `callGroup(group, method, ...args)` - Godot's `call_group`: calls `method(...args)` on every
165
+ component of every node in the group that has a function of that name, and returns how many
166
+ calls it made. The node list is taken before the first call. A component without the method
167
+ is skipped; when the group has nodes but no component has the method, one warning names the
168
+ group and the method (no throw). An empty group calls nothing, silently.
169
+
170
+ ```ts
171
+ for (const enemy of this.scene.getNodesInGroup('enemies')) enemy.visible = false;
172
+ this.scene.callGroup('enemies', 'onAlarm', 3);
173
+ ```
162
174
 
163
175
  #### 6.19.3 Serialization
164
176
 
@@ -3,7 +3,7 @@ name: pix3-scripts
3
3
  description: How to write a Pix3 game script (scripts/*.ts, `export class X extends Script`, attached as `user:X`) — lifecycle, the required getPropertySchema, config, node lookup, signals, transforms, spawning, input, juice/tweens/audio, 2D physics, scene changes, the built-in core:* components and their config keys (reference.md), and the traps that compile clean and break at runtime. Use BEFORE writing or editing any file in scripts/ or attaching a core: component.
4
4
  ---
5
5
 
6
- <!-- Pix3 agent kit 1.6.4 -->
6
+ <!-- Pix3 agent kit 2.0.0-alpha.0 -->
7
7
 
8
8
  # Writing a script
9
9
 
@@ -13,7 +13,8 @@ it. Import only from `@pix3/runtime`, `three`, and other files under the scripts
13
13
  against the runtime's declarations (`.pix3/types/`, or your `node_modules` when the project
14
14
  has its own `tsconfig.json`); your editor sees the same types through the root `tsconfig.json`.
15
15
 
16
- `reference.md` beside this file has the engine's own API map, the capability catalog and
16
+ `reference.md` beside this file has the rest of the API (wheel and multi-touch input, the full 2D
17
+ physics surface, `registerGameDebug`, cutscenes, multiplayer), the capability catalog and
17
18
  **every `core:` component with its config keys and defaults** — reach for a built-in before
18
19
  writing a script for the same effect.
19
20
 
@@ -130,7 +131,8 @@ before children's.
130
131
  `node.position = …` throws at runtime.
131
132
  - 2D space: design pixels, origin centre, X right, **Y up**. YAML rotation is degrees;
132
133
  `rotation.z` is radians.
133
- - `visible`, `name`, `id`, `children`, `parentNode`, `findById(id)`, `findByName(name)`,
134
+ - `visible`, `name`, `nodeId` (the scene id; `id` is three.js's number), `children`,
135
+ `parentNode`, `findById(nodeId)`, `findByName(name)`,
134
136
  `adoptChild(child)`, `queueFree()` (safe inside `onUpdate`), `getComponent(Class)`,
135
137
  `addComponent(c)`, `removeComponent(c)`.
136
138
  - 2D node props as fields: `width`, `height`, `opacity`, `zIndex`, `blendMode`. Assigning
@@ -155,16 +157,17 @@ before children's.
155
157
  `node.disconnect('name', this, handler)`. Every handler connected with `this` as the target
156
158
  is **auto-disconnected on detach, like own-node signals** — including ones on another node
157
159
  (`gameRoot.connect('score', this, this.onScore)`). Window listeners, store subscriptions and
158
- timers are still yours to clean up in `onDetach` (see "Hosting an existing game"). Keep the
160
+ timers are still yours to clean up in `onDetach` ("Hosting an existing game", below). Keep the
159
161
  handler in a field (no `.bind(this)`) so an early manual `disconnect` gets the same function.
160
162
  - UI controls (`Button2D`, `Slider2D`, `Checkbox2D`, `Joystick2D`, …) emit `pressed` (touch
161
163
  went down inside), `released`, `click` (down and up inside — a completed tap),
162
164
  `pointerdown`, `pointerup`; `Checkbox2D` also `toggled`; `Label2D` emits
163
- `typewriter-complete`. The recipes wire buttons to `pressed` (instant); `click` can be
164
- cancelled by sliding off — use it for menus and anything costly.
165
- - Recipe scripts talk through signals on `game-root` (`touch-scored`, `touch-damaged`,
166
- `score-changed`, `lives-changed`, `time-changed`, `game-won`, `game-lost`). Listen to
167
- them; add new signal names for new mechanics; never rename existing ones.
165
+ `typewriter-complete`. Wire game buttons to `pressed` (instant); `click` can be
166
+ cancelled by sliding off — use it for menus and anything costly (purchases):
167
+ `button.connect('click', this, this.onBuy)`.
168
+ - Scripts talk through signals on a shared node such as `game-root` (`score-changed`,
169
+ `lives-changed`, `game-won`, `game-lost`, …). Listen to the ones that exist; add new signal
170
+ names for new mechanics; never rename existing ones.
168
171
 
169
172
  ## `this.scene`
170
173
 
@@ -175,7 +178,8 @@ before children's.
175
178
  - Spawn: `const n = await scene.instantiate('res://scenes/prefabs/x.pix3scene', { parent, instanceId })`
176
179
  (`parent` = node | query; prefab = a scene file with one root). Despawn: `n.queueFree()`.
177
180
  - Scene change: `await scene.changeScene('res://scenes/menu.pix3scene', { transition: 'fade' | 'none', durationSec: 0.3, onLoaded })`
178
- (`durationSec` is each of fade-out and fade-in).
181
+ (`durationSec` is each of fade-out and fade-in). State that must survive it is an autoload:
182
+ `scene.getAutoload(GameState)` (recipe: `pix3-scene-format/project-files.md` → "An autoload").
179
183
  - Time: `scene.time.hitstop(ms)` — no options; only on a contact START, never per frame.
180
184
  `scene.time.slowMotion(scale, { durationMs, blendMs })` — both real-time ms; no `durationMs`
181
185
  = until `scene.time.reset()`.
@@ -201,18 +205,18 @@ before children's.
201
205
  Sound without the editor: `pix3 sfx coin` writes `audio/coin.wav` (presets `coin`, `jump`,
202
206
  `hit`, `explosion`, `powerup`, `click`, or words: `pix3 sfx "short high coin pickup" --out audio/pickup.wav`;
203
207
  `--seed <n>` for a variation) — offline, no key; `.wav` plays everywhere `.ogg`/`.mp3` do.
204
- Sound cannot be proven audible from the live channel: the browser only starts Web Audio after
205
- a real user gesture, and `game_input`'s synthetic taps/keys are not one. Report a sound as
208
+ Sound cannot be proven audible from the bridge: the browser only starts Web Audio after
209
+ a real user gesture, and a synthetic click from chrome-devtools-mcp is not one. Report a sound as
206
210
  "code path verified, audibility not" and ask the human to listen.
207
211
  - Overlap queries without physics response: `scene.collision2d.overlapPoint(x, y, group?)`,
208
212
  `overlapCircle(x, y, r, group?)`, `overlapRect(cx, cy, w, h, group?)`, `raycast(…)` over
209
- nodes carrying `core:Hitbox2D` (the tapper/arena recipes use this).
213
+ nodes carrying `core:Hitbox2D` (tappers, collectors, dodgers).
210
214
  - 2D physics with response: `core:PhysicsBody2D` + `core:Collider2D` on the same node;
211
215
  `scene.physics2d.getBody(node)` → `setVelocity`, `applyImpulse`, `teleport`. Never
212
- hand-write a 2D solver, never import rapier for 2D. (The bouncer recipe runs its own
213
- swept solver in `BallBody.ts` — do not add engine physics to its ball.)
216
+ hand-write a 2D solver, never import rapier for 2D. (A game that already runs its own
217
+ solver keeps it — do not add engine physics on top of it.)
214
218
  - Intents: `scene.commands.register('name', handler, { description })`, `dispatch(name)`.
215
- The recipes register theirs and publish state with `registerGameDebug` in `GameRules` —
219
+ Register one per game and publish state with `registerGameDebug` from one rules script —
216
220
  when you add a mechanic, add its field to that snapshot instead of registering a second one.
217
221
 
218
222
  ## `this.input`
@@ -223,10 +227,10 @@ before children's.
223
227
  `scene.getPointer2DWorldPosition()` for world), `input.isPointerOverUI(pointerId)`,
224
228
  `input.getButton(name)`, `input.getAxis(name)`.
225
229
  - Keys held: `input.getButton('Key_ArrowLeft')`, `getButton('Key_D')` — `Key_` + the
226
- `KeyboardEvent.code` (this is what the recipes' controllers use). In `input.keyEvents`
230
+ `KeyboardEvent.code` (the form player controllers use). In `input.keyEvents`
227
231
  compare `event.code` (`'KeyW'`, `'ArrowUp'`, `'Space'`) — case-sensitive.
228
232
 
229
- ## Hosting an existing game (not a recipe)
233
+ ## Hosting an existing game
230
234
 
231
235
  - **Use the game's own channel.** Before wiring a script, find how the game talks:
232
236
  `grep -rn "CustomEvent\|dispatchEvent\|addEventListener\|subscribe(\|EventTarget\|emit(" src scripts`.
@@ -259,23 +263,29 @@ before children's.
259
263
  (or on any node the game positions itself) is overwritten every frame and never shows. Use the
260
264
  game's own shake (`grep -rn "shake" src scripts`) when it has one; the same goes for its own
261
265
  sound and tween helpers.
262
- - The project's own `AGENTS.md` wins on process (planning first, where config goes); this kit
263
- wins on Pix3 facts (YAML, runtime API, `pix3 check`).
264
266
 
265
267
  ## Traps (compile clean, break at runtime)
266
268
 
267
269
  - Assigning `position` / `rotation` / `scale` throws. Mutate them.
268
270
  - A component that throws in `onStart` / `onUpdate` is **auto-disabled** and the game keeps
269
271
  running — the thing just freezes. Guard lookups (`if (!node) { console.warn(...); return; }`).
270
- With the live channel, `read_errors` after a run shows it; without, tell the human to look
271
- at the editor's console.
272
+ `pix3_errors` right after starting play (or `pix3 smoke`'s `E_SMOKE_SCRIPT`) shows the throw —
273
+ read it before judging anything else.
272
274
  - `as any` on `this.node` hides exactly the error above, and from `pix3 check`. Use
273
275
  `instanceof` narrowing.
274
276
  - Hitstop every frame of an overlap freezes the game — edge-trigger it.
275
277
  - A hidden `UIControl2D` takes no input; `onUpdate` still runs on hidden nodes.
276
278
  - `getComponent('user:X')` is wrong — pass the class.
277
- - Ending a run belongs to the recipe's `GameRules`: call its `finish(won)`; do not show the
278
- result overlay yourself (RETRY would stay disabled).
279
+ - `event.key` depends on the layout and on Shift — match `event.code`.
280
+ - **Moves the wrong way** (sideways, backwards) is a math bug, and flipping signs does not
281
+ converge. With world +Y up, `rotation.z` turns the node's local +Y (its nose) to
282
+ `(-sin θ, cos θ)`: forward is `vx = -Math.sin(rot) * speed`, `vy = Math.cos(rot) * speed`, and
283
+ aiming the nose along `(dx, dy)` is `rotation.z = Math.atan2(-dx, dy)`.
284
+ - **A button does nothing**: something must `connect` to its signal (`pressed` / `click`) on the
285
+ node id the scene really has — check the id in `pix3 tree`, then that the script carrying the
286
+ handler is attached and `enabled`.
287
+ - Ending a run belongs to the one script that owns the rules: call it; do not show the result
288
+ overlay from a second place.
279
289
 
280
290
  ## Known gaps
281
291
 
@@ -1,22 +1,21 @@
1
- <!-- Pix3 agent kit 1.6.4 — 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. -->
1
+ <!-- Pix3 agent kit 2.0.0-alpha.0 — generated from docs/nodes-and-systems.md, packages/cli/kit-includes/engine-api-map.md and the runtime's component registry. Do not edit: pix3 kit --update regenerates it. -->
2
2
  # Script API reference
3
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.
4
+ Generated from the engine's own documentation and code; `SKILL.md` beside it is the practical
5
+ part. Grep a heading instead of reading it whole. The runtime's declarations are in
6
+ `.pix3/types/@pix3/runtime/` (or `node_modules/@pix3/runtime` when the project has its own
7
+ `tsconfig.json`). Source paths quoted here (`src/…`, `packages/…`) are files of the Pix3 engine
8
+ repository, not of this project.
10
9
 
11
10
  ## The surface a `Script` sees
12
11
 
13
12
  Inside any `Script` subclass:
14
13
 
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)`.
16
- - `this.scene` — the `SceneService` (all of §4's `scene.*` APIs, plus `getActiveCamera()`, `getActiveCamera2D()`, `findNode(query)`, `getRootNodes()`, `getViewportInfo()`/`onViewportChanged()`/`isPortrait()`, `raycastViewport(nx,ny)`, `getAudioService`/`getAssetLoader`/`getResourceManager`/`getECSService`, plus `network` and `netNodes` for multiplayer, and `commands` for named game intents). May be `undefined` in some editor previews — guard it.
14
+ - `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 scene YAML only. To fetch by hand: `node.components.find(c => c instanceof CarController)`.
15
+ - `this.scene` — the `SceneService` (all of §4's `scene.*` APIs, plus `getActiveCamera()`, `getActiveCamera2D()`, `findNode(query)`, `getRootNodes()`, `getNodesInGroup(group)` / `callGroup(group, method, ...args)` for node groups (spec §6.19), `getViewportInfo()`/`onViewportChanged()`/`isPortrait()`, `raycastViewport(nx,ny)`, `getAudioService`/`getAssetLoader`/`getResourceManager`/`getECSService`, plus `network` and `netNodes` for multiplayer, `commands` for named game intents, and `getAutoload(nameOrClass)` for the project's autoload singletons, §6.17 of the spec). May be `undefined` in some editor previews — guard it.
17
16
  - `this.input` — the `InputService` (§4 Input).
18
17
  - `this.findNode(query)` — resolve another node by id / name / slash-path, or `null` if absent (`get_node_or_null`).
19
- - `this.getNode(query)` — same lookup but **throws** if the node is missing (`get_node`). In the in-editor code editor the argument autocompletes to the node names/paths of the open scenes and the return type is the exact node type (`this.getNode('Hero')` → `Sprite2D`), à la Godot's `$Node` / WPF `x:Name`. Any other string resolves to `NodeBase`, so a script reused in a scene that lacks the name still type-checks — the names are hints, never constraints. (Typed names come from the editor augmenting `SceneNodeNames`; it's empty in exported games, where only `getNode<T>(query)` applies.)
18
+ - `this.getNode(query)` — same lookup but **throws** if the node is missing (`get_node`); `getNode<T>(query)` types the result.
20
19
 
21
20
  **Lifecycle:** `onAttach(node)` → `onStart()` (first frame) → `onUpdate(dt)` (every
22
21
  frame, `dt` is scaled game time) → `onDetach()`. Define `static getPropertySchema()`
@@ -51,154 +50,82 @@ you never clone textures yourself. Author the region once and drive it from
51
50
  throwing `tickEditorPreview` disables the component and surfaces the error like a
52
51
  play-mode hook, so the editor keeps running.
53
52
 
54
- ## Engine API map
55
-
56
- ### A `Script`
57
-
58
- - Lifecycle: `onAttach(node)` → `onStart()` (first frame) → `onUpdate(dt)` (scaled seconds;
59
- frozen by hitstop) → `onDetach()`. Defaults in the constructor's `this.config = {…}`;
60
- `static getPropertySchema()` exposes them to the inspector / `set_component_property`.
61
- - `this.node` (owner `NodeBase`), `this.scene` (`SceneService`, guard for `undefined` in
62
- editor previews), `this.input` (`InputService`), `this.findNode(q)` (id / name / path →
63
- node or null), `this.getNode(q)` (throws when missing).
64
- - Reach another script: `import { Other } from './Other'; this.node.getComponent(Other)` —
65
- the class, never the `user:Other` string.
53
+ ## Engine API map — beyond `SKILL.md`
66
54
 
67
55
  ### Nodes
68
56
 
69
- - Transform is three.js and **read-only by reference**: `node.position.set(x, y, 0)`,
70
- `node.position.x += dx`, `node.rotation.z = radians`, `node.scale.set(s, s, 1)`. Never
71
- assign `node.position = …`.
72
- - `visible`, `name`, `id`, `children`, `parentNode`, `findById(id)`, `findByName(name)`,
73
- `adoptChild(child)` (runtime parenting), `queueFree()` (safe inside `onUpdate`; use
74
- `dispose()` only outside the tick), `getComponent(Class)`, `addComponent(c)`,
75
- `removeComponent(c)`.
76
- - Signals: `node.emit(name, ...args)`, `node.connect(name, target, fn)`,
77
- `node.disconnect(name, target, fn)`. A script's handlers (`target === this`) are
78
- auto-disconnected on detach — on its own node and on any other node it connected to.
79
- Window listeners, store subscriptions and timers are still yours to clean up.
80
- **UI controls emit:** every `UIControl2D` (`Button2D`, `Slider2D`, `Checkbox2D`, `Joystick2D`,
81
- `InventorySlot2D`, `ScrollContainer2D`) → `'pressed'`, `'released'`, `'click'` (a completed
82
- tap — wire buttons to THIS), `'pointerdown'`, `'pointerup'`; `Checkbox2D` / `InventorySlot2D`
83
- also `'toggled'`; `Label2D` → `'typewriter-complete'`. Read a control's value from its
84
- properties (`Slider2D.value`, `Checkbox2D.checked`) inside the handler.
85
- - **Node2D** adds design-pixel space (origin centre, X right, **Y up**): `width`, `height`,
86
- `opacity` (0..1), `zIndex` (higher draws on top; ties = tree order, later sibling on top),
87
- `blendMode` (`normal | additive | multiply | subtract` — `additive` for glow/VFX), anchored
88
- `layout`. Construct at runtime with the same props the scene YAML uses:
89
- `new ColorRect2D({ id, width, height, color })`, `new Sprite2D({ id, texturePath:
90
- 'res://sprites/ph-circle.png', width, height })`, `new Label2D({ id, label, labelFontSize,
91
- labelColor })`, `new Group2D({ id, width, height })`, then `parent.adoptChild(node)`.
57
+ - `queueFree()` is safe inside `onUpdate`; `dispose()` only outside the tick.
58
+ - Every `UIControl2D` (`Button2D`, `Slider2D`, `Checkbox2D`, `Joystick2D`, `InventorySlot2D`,
59
+ `ScrollContainer2D`) emits `'pressed'`, `'released'`, `'click'`, `'pointerdown'`,
60
+ `'pointerup'`; `Checkbox2D` / `InventorySlot2D` also `'toggled'`. Read a control's value from
61
+ its properties (`Slider2D.value`, `Checkbox2D.checked`) inside the handler.
62
+ - Constructed at runtime with the scene YAML's property names, a `Sprite2D` too:
63
+ `new Sprite2D({ id, texturePath: 'res://sprites/ph-circle.png', width, height })`.
92
64
  - 2D node types: `ColorRect2D`, `Sprite2D`, `TiledSprite2D`, `AnimatedSprite2D`, `Group2D`,
93
- `Label2D` (`label`, `labelFontSize`, `labelColor`, `glowColor`/`glowStrength`,
94
- `outlineColor`/`outlineWidth`), `Button2D`, `Slider2D`, `Checkbox2D`, `Bar2D`, `Joystick2D`,
95
- `InventorySlot2D`, `ScrollContainer2D`, `Camera2D`, `CanvasLayer2D` (HUD layer, drawn after
96
- post-fx, never blooms), `SpineSkeleton2D`, `Layout2D`.
65
+ `Label2D`, `Button2D`, `Slider2D`, `Checkbox2D`, `Bar2D`, `Joystick2D`, `InventorySlot2D`,
66
+ `ScrollContainer2D`, `Camera2D`, `CanvasLayer2D` (HUD layer, drawn after post-fx, never
67
+ blooms), `SpineSkeleton2D`.
97
68
  - 3D node types: `Node3D`, `Camera3D`, `VirtualCamera3D`, `GeometryMesh`, `MeshInstance`,
98
69
  `InstancedMesh3D`, `Sprite3D`, `AnimatedSprite3D`, `Particles3D`, lights (`Ambient`,
99
70
  `Hemisphere`, `Directional`, `Point`, `Spot`), `PostProcess` (bloom / vignette /
100
71
  chromatic aberration; `affect2D: true` for 2D scenes), `AudioPlayer`.
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
72
 
104
73
  ### `this.scene`
105
74
 
106
- - Lookup: `findNode(q)`, `findNodeById`, `findNodeByName`, `findNodeByPath`, `getRootNodes()`,
107
- `getActiveCamera()`, `getActiveCamera2D()`, `getViewportInfo()`, `isPortrait()`.
108
- - **Pointer in 2D world space:** `scene.getPointer2DWorldPosition()` → `Vector2 | null`
109
- (design pixels, Y up; the primary finger), `getPointer2DWorldPosition(pointerId)` for one
110
- finger (null once it is up). Godot's `get_global_mouse_position()`.
111
- - Spawning: `await scene.instantiate('res://prefabs/x.pix3scene', { parent: 'container' })`
112
- → the new node (prefab = a `.pix3scene` with one root). Despawn with `node.queueFree()`.
113
- - Scenes & transitions: `await scene.changeScene('res://scenes/menu.pix3scene', {
114
- transition: 'fade' | 'none', durationSec: 0.3 })`; `scene.fadeToBlack(sec, onDone?)`,
75
+ - Cameras: `getActiveCamera()`, `getActiveCamera2D()`. `getPointer2DWorldPosition(pointerId)`
76
+ is null once that finger is up (Godot's `get_global_mouse_position()` for the primary one).
77
+ - Autoloads by name too: `scene.getAutoload<GameState>('GameState')`.
78
+ - Groups (a node's `groups: [enemies]` in the scene, `node.addToGroup('enemies')` at run time):
79
+ `scene.getNodesInGroup('enemies')` lists the running scene's members in tree order;
80
+ `scene.callGroup('enemies', 'onAlarm', 3)` calls that method on every component that has it
81
+ (returns the count, warns once when none has it).
82
+ - Real-time overlays that survive hitstop: `scene.fadeToBlack(sec, onDone?)`,
115
83
  `scene.fadeFromBlack(sec, onDone?)`, `scene.switchCameraWithFade(cameraId, outSec, inSec,
116
- onDone?)`; `scene.flash({ color, intensity, durationSec })`. Real-time overlays — they
117
- survive hitstop.
118
- - Time: `scene.time.hitstop(ms)` (edge-triggered — on a contact START, never per frame),
119
- `scene.time.slowMotion(scale, { durationMs, blendMs })`, `setScale`, `reset`, `scale`,
120
- `isFrozen`.
121
- - **Juice (`scene.juice`)** — one-liners, call them WITH the mechanic: `shake(target, {
122
- amplitude: 8, frequency: 24, duration: 0.35, decay: 1.5 })`, `punchScale(target, { amount: 0.3,
123
- duration: 0.35, vibrato: 3 })`, `popIn(target, { from: 0, duration: 0.4, easing: 'backOut' })`
124
- (these three take `duration`, not `durationSec`), `flash({ color: '#ffffff', intensity: 1,
125
- durationSec: 0.2 })`, `burst(anchor, { count: 14,
126
- speed: 260, spread: 2π, direction, lifeSec: 0.5, color | colors: [], sizePx: 10, gravityY:
127
- -600, fadeOut: true, additive: true, zIndex })`, `floatText('+25', { at, color,
128
- fontSizePx: 28, driftPx: 60, durationSec: 0.8, glow })`. `target` = node | node query |
129
- `'camera'` / `'camera2d'`; `anchor` = node | query | `{ x, y }` 2D world point. Presets as
130
- components: `core:BurstOnSignal`, `core:SfxOnSignal`, `core:Shake`, `core:PunchScale`,
131
- `core:PopIn`, `core:Fade`.
132
- - **Tweens (`scene.tween`)** — Godot-style, on scaled time: `scene.tween.to(target, props, {
133
- durationSec: 0.3, ease: 'cubicOut', delaySec, yoyo, repeat (-1 = forever), onUpdate,
134
- onComplete })` → `{ cancel(), finished: Promise<'completed' | 'cancelled'>, isRunning }`.
135
- `props` for a node: `x`, `y`, `position: {x, y}`, `scale` (number = uniform, or `{x, y}`),
136
- `rotation` (radians), `opacity`, `width`, `height`, or any numeric dotted path; for a plain
137
- object every key is a dotted path. `fadeIn(node, sec)`, `fadeOut(node, sec, { hide: true })`,
138
- `crossFade(from, to, sec)` (state/panel switches — the in-scene counterpart of
139
- `changeScene`'s fade), `killAll(target?)`. Use these instead of hand-lerping in `onUpdate`.
140
- - **Trail (`scene.juice.trail`)**: `scene.juice.trail(node, { lifeSec: 0.35, widthPx: 14,
141
- color | colors: [], additive: true, zIndex, maxPoints: 48 })` → a fading ribbon that follows
142
- the node (a ball, a projectile); `trail.stop()` lets it fade out; freeing the node frees it.
143
- - **Audio (`scene.audio`)**: `sfx(preset, { volume, pitch })` — procedural, no asset; presets
144
- `tap | score | bounce | explosion | powerup | win | lose | laser | tick`;
145
- `await play('res://sfx/hit.ogg', { bus: 'sfx', pitchVariation: 0.1, volumeVariation: 0.1 })`
146
- for an authored clip; `setBusVolume(bus, v, fadeSec)`, `applySnapshot(name)`, `stopAll()`.
147
- - **2D physics (`scene.physics2d`)** — the built-in rigid-body solver; never hand-write one,
148
- never import rapier for 2D. Author Unity-style: `core:PhysicsBody2D` (`static | kinematic |
149
- dynamic`, `mass`, `restitution`, `friction`, `linearDamping`, `fixedRotation`, `bullet`,
150
- `emitContacts`) **and** `core:Collider2D` (`rect | circle | polygon | capsule`, `isSensor`,
151
- `group`) on the same node; a `core:Collider2D` alone is static geometry. Gravity is
152
- negative Y (`core:PhysicsWorld2D` on the root or `scene.physics2d.setGravity(0, -1800)`;
153
- top-down = `(0, 0)`). From a script: `const body = scene.physics2d.getBody(node)` →
154
- `setVelocity(vx, vy)`, `applyImpulse(ix, iy)`, `applyForce`, `teleport(x, y, rot?)`, `wake()`,
155
- `velocityX/Y`, `isSleeping`; queries `raycast(...)`, `overlapCircle(...)`,
156
- `overlapRect(...)`, `moveAndSlide(...)`; signals on the node: `body-entered` /
157
- `body-exited` (sensors), `contact-started` / `contact-ended` (with `emitContacts`).
158
- Joints: `core:RevoluteJoint2D`.
159
- - 2D overlap-only queries (no response): `scene.collision2d.overlapPoint(x, y, group?)`,
160
- `overlapCircle(x, y, r, group?)`, `overlapRect(cx, cy, w, h, group?)`, `raycast(x1, y1, x2,
161
- y2, group?)` over nodes carrying `core:Hitbox2D`.
162
- - Game intents: `scene.commands.register(name, handler, { description })`,
163
- `dispatch(name, args)`, `list()`, `log` — how tests and the menu drive the game without
164
- clicking.
165
- - Debug surface for `game_observe` / `game_run`: `import { registerGameDebug } from
166
- '@pix3/runtime'`; in `onStart`: `this.disposeDebug = registerGameDebug({ name, version: 1,
167
- snapshot: () => ({ …JSON-safe state… }), inspect: (query, args) => …, action: (name, args)
168
- => … })`, call the returned disposer in `onDetach`. Global and last-wins — only the running
169
- scene's flow/rules script registers one; recipes already do (extend theirs, do not add a
170
- second).
171
- - Cutscenes: `scene.cutscene.playCinematic(id, {…})`; camera blends via `core:CameraBrain`
172
- + `VirtualCamera3D`.
84
+ onDone?)`, `scene.flash({ color, intensity, durationSec })`.
85
+ - Time: `scene.time.setScale`, `reset`, `scale`, `isFrozen` beside `hitstop` / `slowMotion`.
86
+ - Juice presets as components: `core:BurstOnSignal`, `core:SfxOnSignal`, `core:Shake`,
87
+ `core:PunchScale`, `core:PopIn`, `core:Fade`.
88
+ - Tweens: `finished` is a `Promise<'completed' | 'cancelled'>`; `props` may also be `scale` as a
89
+ number (uniform) or `{x, y}`, or any numeric dotted path; on a plain object every key is a
90
+ dotted path. `crossFade` is the in-scene counterpart of `changeScene`'s fade (state and panel
91
+ switches). A trail is freed with its node.
92
+ - Audio: `setBusVolume(bus, v, fadeSec)`, `applySnapshot(name)`, `stopAll()`.
93
+ - **2D physics (`scene.physics2d`)**: `core:PhysicsBody2D` (`static | kinematic | dynamic`,
94
+ `mass`, `restitution`, `friction`, `linearDamping`, `fixedRotation`, `bullet`, `emitContacts`)
95
+ **and** `core:Collider2D` (`rect | circle | polygon | capsule`, `isSensor`, `group`) on the
96
+ same node; a `core:Collider2D` alone is static geometry. Gravity is negative Y
97
+ (`core:PhysicsWorld2D` on the root or `scene.physics2d.setGravity(0, -1800)`; top-down =
98
+ `(0, 0)`). A body: `applyForce`, `wake()`, `velocityX/Y`, `isSleeping` beside `setVelocity`,
99
+ `applyImpulse`, `teleport(x, y, rot?)`; queries `raycast(...)`, `overlapCircle(...)`,
100
+ `overlapRect(...)`, `moveAndSlide(...)`; signals on the node: `body-entered` / `body-exited`
101
+ (sensors), `contact-started` / `contact-ended` (with `emitContacts`). Joints:
102
+ `core:RevoluteJoint2D`.
103
+ - Game intents: `scene.commands` also has `list()` and `log` — how tests and the menu drive the
104
+ game without clicking.
105
+ - The debug surface `pix3_game_run`'s `gameState` predicates read: `import { registerGameDebug }
106
+ from '@pix3/runtime'`; in `onStart`: `this.disposeDebug = registerGameDebug({ name, version: 1,
107
+ snapshot: () => ({ …JSON-safe state… }), inspect: (query, args) => …, action: (name, args) =>
108
+ … })`, call the returned disposer in `onDetach`. Global and last-wins — only the running
109
+ scene's flow/rules script registers one (extend it, do not add a second).
110
+ - Cutscenes: `scene.cutscene.playCinematic(id, {…})`; camera blends via `core:CameraBrain` +
111
+ `VirtualCamera3D`.
173
112
  - Multiplayer: `scene.network`, `scene.netNodes`, `core:NetworkedNode`,
174
113
  `core:ReplicatedTransform` (only when the brief says multiplayer).
175
114
 
176
115
  ### `this.input`
177
116
 
178
- - Per frame: `input.pointerEvents` → `{ type: 'down' | 'move' | 'up' | 'cancel', pointerId,
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.
183
- - Polled: `input.isPointerDown` (any finger), `input.pointerPosition` (primary finger,
184
- canvas space — convert with `scene.getPointer2DWorldPosition()`), `input.getActivePointers()`,
185
- `input.getPointer(id)`, `input.pointerDownCount`, `input.isPointerOverUI(pointerId)` (gate
186
- gestures on THIS, not on `isHoveringUI`), `input.getButton(name)` / `getAxis(name)`
187
- (`Action_Primary`, arrows / WASD axes), `lock()` / `unlock()`.
188
- - Keyboard: compare `event.code` (`'KeyW'`, `'ArrowUp'`), case-sensitive.
189
- - A tap-to-place mechanic is: on a `'down'` event, `scene.getPointer2DWorldPosition(ev.pointerId)`,
117
+ - Wheel: `input.wheelDelta` (plain scroll wheel only); `input.wheelZoomDelta` (Ctrl/⌘ + wheel
118
+ and trackpad pinch — use it for camera zoom, it is excluded from `wheelDelta`);
119
+ `input.wheelModifiers` (`{ ctrl, meta, shift, alt }`). The canvas already `preventDefault()`s
120
+ every wheel event, so ctrl-wheel never zooms the page.
121
+ - Fingers: `input.getActivePointers()`, `input.getPointer(id)`, `input.pointerDownCount`. Gate
122
+ gestures on `input.isPointerOverUI(pointerId)`, not on `isHoveringUI`.
123
+ - Actions: `getButton('Action_Primary')`, the arrows / WASD axes of `getAxis(name)`;
124
+ `lock()` / `unlock()`.
125
+ - A tap-to-place mechanic: on a `'down'` event, `scene.getPointer2DWorldPosition(ev.pointerId)`,
190
126
  convert to the board's local space (`board.worldToLocal` or subtract the board's world
191
127
  position), place the node, `scene.juice.burst({ x, y })`, `scene.audio.sfx('tap')`.
192
128
 
193
- ### Traps
194
-
195
- - Assigning `position` / `rotation` / `scale` throws — mutate them.
196
- - A component that throws in `onStart` / `onUpdate` is auto-disabled and the game keeps
197
- running — `read_errors` after `play_start` is the only way to see it.
198
- - `this.node as any` disables exactly the check that would have caught the above.
199
- - Hitstop every frame while an overlap lasts freezes the contact forever — edge-trigger it.
200
- - A hidden `UIControl2D` takes no input; `tick` still runs on hidden nodes.
201
-
202
129
  ## Capability catalog — nodes
203
130
 
204
131
  Add via the editor **Create** menu / `Create*Command`, or author in `.pix3scene`
@@ -241,7 +168,7 @@ tables: node-types-reference.md.
241
168
  }
242
169
  ```
243
170
 
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.
171
+ **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. **`pix3 character-compile <spec.yaml>`** (`packages/cli/src/character/`) emits exactly this shape from frame PNGs named in a small spec (`clips: [{ variant, state, frames | sequence, fps?, loop? }]`) — a managed sprite folder (`sprites/<slug>/<slug>.pix3anim` + `<variant>_<state>_<nnnn>.png`, the frames copied in) plus `scenes/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. It never overwrites a file with other content without `--force`, and `--dry-run` shows the plan.
245
172
 
246
173
  **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.
247
174
 
@@ -302,7 +229,7 @@ cost is per skeleton per frame, so budget dozens, not hundreds. Full property ta
302
229
  Attach in the inspector or in YAML `components:`. These are the pre-built,
303
230
  designer-facing behaviors — prefer them over writing a script for the same
304
231
  effect. Registered in
305
- packages/pix3-runtime/src/behaviors/register-behaviors.ts.
232
+ packages/runtime/src/behaviors/register-behaviors.ts.
306
233
 
307
234
  | Component id | Does |
308
235
  | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -341,7 +268,7 @@ so a keyframe **event track** or a script `emit()` can fire them.
341
268
  (3D-only), `core:uv-scroll`, `core:flash`, `core:adjust`
342
269
  (brightness/contrast/saturation), `core:grayscale`, `core:tint`. Params are
343
270
  keyframe-animatable. See
344
- packages/pix3-runtime/src/shader-effects/.
271
+ packages/runtime/src/shader-effects/.
345
272
 
346
273
  ## `core:` components — every config key
347
274