@pix3/cli 1.6.3 → 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,9 +1,9 @@
1
1
  ---
2
2
  name: pix3-scene-format
3
- description: The .pix3scene YAML format — file header, node shape (id/type/name/properties/children/components), component shape, 2D transform and layout blocks, res:// references, textures, prefab instances and overrides, scenes/ui overlays and the visible/initiallyVisible split, plus how to translate design/recipe.md tool names into file edits. Use BEFORE creating or editing any .pix3scene file (scene, prefab or overlay) in this project.
3
+ description: The .pix3scene YAML format — file header, node shape (id/type/name/properties/children/components), component shape, 2D transform and layout blocks, res:// references, textures, prefab instances and overrides, scenes/ui overlays and the visible/initiallyVisible split; `.pix3anim` flipbooks (and `pix3 character-compile`) in `pix3anim.md` beside it; creating, renaming, moving, deleting files, extracting a branch into a prefab, locales, `pix3project.yaml` and build options in `project-files.md` beside it. Use BEFORE creating or editing any .pix3scene file (scene, prefab or overlay) in this project.
4
4
  ---
5
5
 
6
- <!-- Pix3 agent kit 1.6.3 -->
6
+ <!-- Pix3 agent kit 2.0.0-alpha.0 -->
7
7
  # `.pix3scene` format
8
8
 
9
9
  Scenes, prefabs and overlays are the same file type: YAML with a `.pix3scene` extension.
@@ -14,19 +14,16 @@ The engine's own statement of the format (principles, skinned UI controls, Spine
14
14
 
15
15
  ## Before you write
16
16
 
17
- - Re-read the file you are about to change — the human may have edited it in the editor.
18
- - Change only the lines you mean to change. A whole-file rewrite from memory drops what the
19
- human (or the editor) wrote since you last read it.
20
- - If your tools support it, write to a temporary file and rename it over the target. Not
21
- required for correctness — the editor waits for the file to stop changing — but cleaner.
22
- - Then `pix3 check` (see `pix3-verify`).
17
+ Sync, re-read the file, change only the lines you mean to change, then `pix3 check` (`AGENTS.md`
18
+ rules 2 and 4). If your tools support it, write to a temporary file and rename it over the
19
+ target — not required (the editor waits for the file to stop changing), but cleaner.
23
20
 
24
21
  ## File
25
22
 
26
23
  ```yaml
27
24
  version: 1.0.0 # the only format version; copy it
28
25
  metadata: # optional, free-form
29
- author: Pix3 Recipe
26
+ author: Pix3
30
27
  description: Tapper gameplay — falling objects
31
28
  root: # list of root nodes; at least one
32
29
  - id: game-root
@@ -64,7 +61,7 @@ root: # list of root nodes; at least one
64
61
  Rules:
65
62
 
66
63
  - **`id`**: short, readable, kebab-case, unique (`combo-label`, `spawner-bonus`). Two nodes
67
- with the same id fail the load. Never rename an id the recipe lists as stable — scripts
64
+ with the same id fail the load. Never rename an id other scripts use — scripts
68
65
  find nodes by id; rename `name` instead.
69
66
  - **`type`**: must be a real node type. An unknown or misspelled type does **not** fail the
70
67
  load — the node becomes an inert placeholder that draws and does nothing.
@@ -83,8 +80,8 @@ transform:
83
80
  rotation: 0 # DEGREES in YAML; positive = counter-clockwise (Y is up)
84
81
  ```
85
82
 
86
- 2D space: origin at the centre of the screen/parent, **X right, Y up**. The recipes use a
87
- 1080 x 1920 portrait design (top edge y = 960, bottom y = -960); other projects read
83
+ 2D space: origin at the centre of the screen/parent, **X right, Y up**. A portrait game uses e.g. a
84
+ 1080 x 1920 design (top edge y = 960, bottom y = -960); other projects read
88
85
  `viewportBaseSize` / `projectType` in `pix3project.yaml` — a `3d` project may have loose
89
86
  top-level 2D nodes (a HUD) and no 2D root at all, so position new HUD relative to the HUD
90
87
  nodes that are already there. Scripts see the rotation in
@@ -102,17 +99,28 @@ layout:
102
99
  enabled: true
103
100
  horizontalAlign: left # left | center | right | stretch
104
101
  verticalAlign: top # top | center | bottom | stretch
102
+ left: 40 # margins, px from the parent's edge to the node's same edge:
103
+ top: 30 # `left` keeps `left`, `right` keeps `right`, `stretch` keeps both
105
104
  ```
106
105
 
107
- The node keeps its authored distance to that edge of its parent when the parent resizes
108
- (a different screen aspect). `stretch` keeps both margins and resizes the node. Position is
109
- still authored in `transform.position`. HUD widgets anchor to screen edges; full-screen
110
- backgrounds and roots use `stretch` on both axes.
106
+ An anchored axis is placed by its margin, and keeps it when the parent resizes (a different
107
+ screen aspect, a container resized in the editor). On that axis the `transform.position`
108
+ component is ignored — write `0` — and under `stretch` the node's `width` / `height` is derived
109
+ from the two margins, so leave it out (a `Checkbox2D`'s `size` / `Joystick2D`'s `radius` too: its
110
+ side is the stretched span). A centred axis has no margin: `position` places it, as
111
+ for any node. A file with the anchors but **no margins** still loads: the margins are read off
112
+ the rect, and the editor rewrites the node in the margin form the first time it saves the file.
113
+ `pix3 tree <scene>` prints the margins and the rect they give at the design size —
114
+ `pos=(-400,900) size=200x60 layout=left/top(left=40,top=30)` — so to move such a node, change
115
+ its margin, not its `position`.
116
+ HUD widgets anchor to screen edges; full-screen backgrounds and roots use `stretch` on both
117
+ axes (a root's reference is `viewportBaseSize`, so a root keeps its full rect, no margins).
111
118
 
112
119
  **A new HUD node must not overlap the HUD that is already there.** Before you place it, read
113
120
  the `position`, `width`/`height` and anchoring of its siblings and pick a free spot (a combo
114
- label placed "top-right" once landed under the game's Shop button). With the live channel,
115
- confirm it with a `viewport_screenshot` while the game runs; without it, ask the human to look.
121
+ label placed "top-right" once landed under the game's Shop button). With the editor open,
122
+ confirm it with `pix3_screenshot` + `take_screenshot` while the game runs (`pix3-editor`);
123
+ without it, ask the human to look.
116
124
 
117
125
  Flow (stack children in a row/column) is a separate block:
118
126
  `flow: { enabled: true, direction: vertical, gap: 16, paddingX: 0, paddingY: 0, align: start, autoSize: false }`.
@@ -126,7 +134,7 @@ are ignored — `pix3 validate` says so); on an **instance** node they are the o
126
134
  - **Colour**: a quoted hex string, `color: "#141a2e"`. Always quote — an unquoted `#` starts
127
135
  a YAML comment and the value becomes empty.
128
136
  - **Asset path**: `res://` + path from the project root (the folder with `pix3project.yaml`),
129
- whatever the layout: `res://sprites/ph-target.png` in a recipe, `res://src/assets/textures/x.png`
137
+ whatever the layout: `res://sprites/ph-target.png` in a new project, `res://src/assets/textures/x.png`
130
138
  in a project that keeps assets under `src/`. Follow the folders the project already has; a
131
139
  new project from `pix3 new` uses one folder per type at the root (`sprites/`, `audio/`,
132
140
  `fonts/`, `models/`, `spine/`, `scripts/`, `scenes/`).
@@ -134,6 +142,9 @@ are ignored — `pix3 validate` says so); on an **instance** node they are the o
134
142
  `textureNormal`, `textureFill`, …). The saver always writes this form; a bare
135
143
  `texturePath: 'res://…'` string is only read for compatibility.
136
144
  - **Strings** with `:` `#` `{` or leading spaces: quote them (`label: "SCORE 0"`).
145
+ - **Flipbook**: an `AnimatedSprite2D` points at a `.pix3anim` (`animationResourcePath`) — a JSON
146
+ file of named clips; its fields and defaults are in `pix3anim.md` beside this file. A character
147
+ with variants and states is compiled, not hand-written: `pix3 character-compile`.
137
148
 
138
149
  ### Shader effects (`Sprite2D`, `AnimatedSprite2D`, `Button2D` only)
139
150
 
@@ -143,7 +154,7 @@ effects:
143
154
  params: { color: "#7ee787", amount: 1 }
144
155
  ```
145
156
 
146
- `core:tint` multiplies a near-white placeholder PNG to the colour — this is how recipe
157
+ `core:tint` multiplies a near-white placeholder PNG to the colour — this is how
147
158
  placeholders get their palette. Also `core:adjust`, `core:grayscale`, `core:flash`,
148
159
  `core:uv-scroll`, `core:dissolve`. Other 2D nodes do not read `effects` (a `ColorRect2D` takes
149
160
  its colour from `color`). On a `GeometryMesh` the stack lives at `material.effects`.
@@ -181,9 +192,10 @@ its colour from `color`). On a `GeometryMesh` the stack lives at `material.effec
181
192
  stays the instance's own id). `findByName('Result Overlay')` finds the instance above, not
182
193
  the name written in `result.pix3scene`; omit `name` to keep the prefab's.
183
194
  - Instance `properties` are applied to the prefab's root through its **schema names**:
184
- `visible`, `opacity`, `width`, `layoutEnabled`, `horizontalAlign`, … plus a `transform` block
185
- (`position`, `rotation`, `scale`). The `layout:` / `flow:` blocks of a plain node are not read
186
- here — write `layoutEnabled: true` + `horizontalAlign: right` instead.
195
+ `visible`, `opacity`, `width`, `layoutEnabled`, `horizontalAlign`, the margins `layoutLeft` /
196
+ `layoutRight` / `layoutTop` / `layoutBottom`, … plus a `transform` block (`position`,
197
+ `rotation`, `scale`). The `layout:` / `flow:` blocks of a plain node are not read here — write
198
+ `layoutEnabled: true` + `horizontalAlign: right` + `layoutRight: 20` instead.
187
199
  - Do **not** add `components` or `children` to an instance node — they are ignored (the
188
200
  content comes from the prefab). To change what is inside, edit the prefab file itself, or
189
201
  override one inner node's properties:
@@ -199,9 +211,11 @@ its colour from `color`). On a `GeometryMesh` the stack lives at `material.effec
199
211
  them by id (`retry-button` inside `scenes/ui/result.pix3scene` is found as `retry-button`).
200
212
  - A prefab carries fixed defaults only (e.g. its `core:Hitbox2D` group). Tunables live on
201
213
  plain nodes in the host scene.
202
- - Where prefabs live: `scenes/prefabs/` (the recipes) or a top-level `prefabs/` — the export
214
+ - Where prefabs live: `scenes/prefabs/` or a top-level `prefabs/` — the export
203
215
  treats both (and `scenes/ui/`) as prefabs, never as a boot scene.
204
216
  - Spawned at runtime with `await this.scene.instantiate('res://scenes/prefabs/x.pix3scene', { parent })`.
217
+ - Turning a branch of a scene into a prefab (the editor has no "Save as Prefab"):
218
+ `project-files.md` → "Extract a branch into a prefab".
205
219
 
206
220
  ## Full-screen UI lives in `scenes/ui/`
207
221
 
@@ -215,23 +229,6 @@ scene with `properties: { visible: false }`. Two different flags, both needed:
215
229
 
216
230
  Never put a full-screen dimmer/panel inline in `main.pix3scene`.
217
231
 
218
- ## Recipe tool names → file edits
219
-
220
- `design/recipe.md` was written for the in-editor agent. Without its tools:
221
-
222
- | recipe.md says | you do |
223
- | --- | --- |
224
- | tunable with `component: "user:X"` | edit `config.<property>` of the component with `type: user:X` on the node with that `id` |
225
- | tunable without `component` | edit `properties.<property>` on the node with that `id` (in the `transform` / `layout` / `flow` block where it belongs) |
226
- | `set_property` | edit `properties` in the YAML |
227
- | `set_component_property` | edit the component's `config` |
228
- | `create_node` / `add_component` | add the node / component entry to the YAML |
229
- | `fs_write` (+ `overwrite: true`) | write the file |
230
- | `play_start`, `game_input`, `game_observe`, `game_run` | the same tools over the live channel when it is connected (`pix3-verify`); otherwise `pix3 check`, then `pix3 smoke <the scene you changed>`, then ask the human to press Play and tell them what to look for |
231
-
232
- Values out of a tunable's `min`/`max` load as written (`pix3 validate` warns); the script's
233
- schema `setValue` may clamp them.
234
-
235
232
  ## Known gaps
236
233
 
237
234
  - `metadata.description` is free text; nothing reads it but humans.
@@ -0,0 +1,185 @@
1
+ <!-- Pix3 agent kit 2.0.0-alpha.0 — the .pix3anim format. Generated by pix3 kit; pix3 kit --update replaces it unless you edited it. -->
2
+ # `.pix3anim` — flipbook animation resources
3
+
4
+ A `.pix3anim` is **JSON**: a set of named clips, each a list of frames. `AnimatedSprite2D`
5
+ plays one: `animationResourcePath: res://…/foo.pix3anim`, `currentClip`, `isPlaying`,
6
+ `sizeMode`, `freeOnFinish` (the node's properties are in `pix3-nodes`; `AnimatedSprite3D` takes
7
+ its own `frames` list instead). The loader
8
+ reads the resource and every frame texture it names, also for a node that arrives through a
9
+ prefab or `scene.instantiate`. **Every field may be omitted**: the loader fills in the default
10
+ in the tables below, so hand-write only what you mean.
11
+
12
+ Keep a flipbook in its own folder, the frames beside it — `sprites/<name>/<name>.pix3anim` +
13
+ `sprites/<name>/<clip>_<nnnn>.png` (the editor's Sprite Editor writes and reads that layout and
14
+ shows the folder as one item). Nothing enforces it: any `res://` path works.
15
+
16
+ ## Create one and bind it
17
+
18
+ The editor does not create a `.pix3anim` (its inspector binds an existing one); you write it:
19
+
20
+ 1. Frames into one folder: `sprites/<name>/<clip>_0001.png`, `<clip>_0002.png`, … (or SVGs —
21
+ by the rules of AGENTS.md rule 5; `pix3 validate` checks the frames a `.pix3anim` names like
22
+ a scene's sprites: `E_SVG_*`, reported on the `.pix3anim`).
23
+ 2. `sprites/<name>/<name>.pix3anim`: `version`, `texturePath: ""` and one clip per animation,
24
+ each frame naming its image — the example under "Two modes" below; leave out every field
25
+ whose default (the tables) is what you want.
26
+ 3. Bind it on the `AnimatedSprite2D` node:
27
+
28
+ ```yaml
29
+ - id: coin-spin
30
+ type: AnimatedSprite2D
31
+ name: Coin Spin
32
+ properties:
33
+ width: 96
34
+ height: 96
35
+ animationResourcePath: res://sprites/coin/coin.pix3anim
36
+ currentClip: spin
37
+ isPlaying: true
38
+ ```
39
+
40
+ A script switches clips with `sprite.play('spin')`.
41
+ 4. `pix3 check`: a wrong `animationResourcePath` is `E_MISSING_RESOURCE`, a frame path that
42
+ points nowhere `E_MISSING_FRAME`, a file that is not JSON `E_ANIM_JSON`; then
43
+ `pix3 smoke <the scene>`.
44
+
45
+ ## Two modes
46
+
47
+ - **Sequence** (one image per frame — the usual case): every frame sets `texturePath`; the
48
+ top-level `texturePath` stays `""`.
49
+ - **Spritesheet**: the top-level `texturePath` is the sheet; each frame leaves its own
50
+ `texturePath` empty and sets the UV rect `offset` + `repeat` (fractions of the sheet). Both
51
+ default to `{ "x": 0, "y": 0 }`, which samples nothing — required in this mode.
52
+
53
+ ```json
54
+ {
55
+ "version": "1.0.0",
56
+ "texturePath": "",
57
+ "clips": [
58
+ {
59
+ "name": "burst",
60
+ "fps": 30,
61
+ "loop": false,
62
+ "frames": [
63
+ { "texturePath": "res://sprites/burst/burst_0001.png" },
64
+ { "texturePath": "res://sprites/burst/burst_0002.png", "durationMultiplier": 2 }
65
+ ]
66
+ }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ ## The fields
72
+
73
+ Types are the runtime's (`AnimationResource.ts`); "Omitted →" is what the loader fills in. A
74
+ field marked `?` is optional in the file format itself.
75
+
76
+ ### `AnimationResource`
77
+
78
+ The file.
79
+
80
+ | Field | Type | Omitted → | Notes |
81
+ | --- | --- | --- | --- |
82
+ | `version` | `string` | `"1.0.0"` | The only version; copy it |
83
+ | `texturePath` | `string` | `""` | Spritesheet mode: `res://` of the sheet. Sequence mode: `""` |
84
+ | `clips` | `AnimationClip[]` | `[]` | Clip names must be unique — scripts address clips by name |
85
+
86
+ ### `AnimationClip`
87
+
88
+ | Field | Type | Omitted → | Notes |
89
+ | --- | --- | --- | --- |
90
+ | `name` | `string` | `clip-<n>` (1-based position) | What `currentClip` and `sprite.play(name)` use |
91
+ | `frames` | `AnimationFrame[]` | `[]` | In play order |
92
+ | `fps` | `number` | `12` | Frames per second; positive |
93
+ | `loop` | `boolean` | `true` | `false`: holds the last frame, `isPlaying` turns false, the node emits `animation-finished` (`freeOnFinish: true` then removes it) |
94
+ | `playbackMode` | `AnimationPlaybackMode` | `"normal"` | `"normal"` or `"ping-pong"` |
95
+
96
+ ### `AnimationFrame`
97
+
98
+ | Field | Type | Omitted → | Notes |
99
+ | --- | --- | --- | --- |
100
+ | `textureIndex` | `number` | `0` | Legacy; leave it 0 |
101
+ | `offset` | `AnimationVector2` | `{"x":0,"y":0}` | Spritesheet mode: UV origin of the frame in the sheet |
102
+ | `repeat` | `AnimationVector2` | `{"x":0,"y":0}` | Spritesheet mode: UV size of the frame in the sheet |
103
+ | `durationMultiplier` | `number` | `1` | This frame lasts `durationMultiplier / fps` seconds |
104
+ | `anchor` | `AnimationVector2` | `{"x":0.5,"y":0.5}` | The point of the frame (0–1, **y from the top**) that lands on the node's position — the feet for a character |
105
+ | `texturePath` | `string` | `""` | Sequence mode: `res://` of this frame's image |
106
+ | `boundingBox` | `AnimationBoundingBox` | `{"x":0,"y":0,"width":0,"height":0}` | Frame rect in source pixels |
107
+ | `collisionPolygon` | `AnimationPolygonPoint[]` | `[]` | Optional outline in frame space |
108
+ | `events?` | `AnimationFrameEvent[]` | `[]` | Signals the node emits when play enters this frame |
109
+ | `sourceSize?` | `AnimationSize` | the `boundingBox` size | Pixel size of the frame image; `sizeMode: native` lays the node out from it (`0×0` = unknown → stretch) |
110
+ | `points?` | `AnimationFramePoint[]` | `[]` | Named sockets (a muzzle, a hand) that move across frames; `core:PointAttachment` parks a child on one |
111
+
112
+ ### `AnimationVector2`
113
+
114
+ | Field | Type | Omitted → | Notes |
115
+ | --- | --- | --- | --- |
116
+ | `x` | `number` | — | |
117
+ | `y` | `number` | — | |
118
+
119
+ ### `AnimationBoundingBox`
120
+
121
+ | Field | Type | Omitted → | Notes |
122
+ | --- | --- | --- | --- |
123
+ | `x` | `number` | — | From the frame's left edge, pixels |
124
+ | `y` | `number` | — | From the frame's top edge, pixels |
125
+ | `width` | `number` | — | |
126
+ | `height` | `number` | — | |
127
+
128
+ ### `AnimationPolygonPoint`
129
+
130
+ | Field | Type | Omitted → | Notes |
131
+ | --- | --- | --- | --- |
132
+ | `x` | `number` | — | |
133
+ | `y` | `number` | — | |
134
+
135
+ ### `AnimationFrameEvent`
136
+
137
+ | Field | Type | Omitted → | Notes |
138
+ | --- | --- | --- | --- |
139
+ | `signal` | `string` | — | Emitted on the node; an empty signal drops the event |
140
+ | `args` | `string` | — | Raw text, parsed when it fires (same convention as the keyframe event track) |
141
+
142
+ ### `AnimationSize`
143
+
144
+ | Field | Type | Omitted → | Notes |
145
+ | --- | --- | --- | --- |
146
+ | `width` | `number` | — | |
147
+ | `height` | `number` | — | |
148
+
149
+ ### `AnimationFramePoint`
150
+
151
+ | Field | Type | Omitted → | Notes |
152
+ | --- | --- | --- | --- |
153
+ | `name` | `string` | — | Unique within the frame (the first wins) |
154
+ | `x` | `number` | — | 0–1 of the frame width |
155
+ | `y` | `number` | — | 0–1 of the frame height, from the top |
156
+ | `angle?` | `number` | — | Degrees, clockwise, 0 = pointing right |
157
+
158
+ `AnimationPlaybackMode` is `"normal" | "ping-pong"`.
159
+
160
+ ## A character with variants and states
161
+
162
+ One `.pix3anim` whose clips are named `<variant>.<state>` (`sword.idle`, `sword.attack`,
163
+ `bow.idle`; a clip without the dot is a variant-less state such as `die`), an `AnimatedSprite2D`
164
+ prefab root with `sizeMode: native`, and `core:CharacterVisual2D` on it (`variant`, `state`).
165
+ Game code calls `playState('attack', { restart: true })` and `setVariant('bow')` and never spells
166
+ a clip name. Do not write it by hand — compile it from the frame images:
167
+
168
+ ```bash
169
+ pix3 character-compile art/goblin.yaml --dry-run # the plan
170
+ pix3 character-compile art/goblin.yaml # sprites/goblin/…, scenes/prefabs/Goblin.pix3scene
171
+ ```
172
+
173
+ ```yaml
174
+ # art/goblin.yaml — frame paths are relative to this file; frames must be PNG
175
+ name: Goblin
176
+ anchor: { x: 0.5, y: 0.9 } # the feet
177
+ clips:
178
+ - { variant: sword, state: idle, fps: 10, frames: [sword/idle_1.png, sword/idle_2.png] }
179
+ - { variant: sword, state: attack, fps: 15, sequence: sword/attack } # sword/attack_<n>.png
180
+ - { state: die, sequence: die/die }
181
+ ```
182
+
183
+ It refuses to overwrite a file with other content (`--force` to replace) and prints the defaults
184
+ it chose (fps 12, one-shot `attack`/`die`/`hit`) as warnings. Then instance the prefab
185
+ (`instance: res://scenes/prefabs/Goblin.pix3scene`) and run `pix3 check`.
@@ -0,0 +1,244 @@
1
+ <!-- Pix3 agent kit 2.0.0-alpha.0 — project files the editor leaves to you. Generated by pix3 kit; pix3 kit --update replaces it unless you edited it. -->
2
+ # Project files the editor leaves to you
3
+
4
+ The Pix3 editor edits a project that is already open: nodes, properties, what is inside a
5
+ prefab, the clips of a `.pix3anim`, the entries of a locale table. It **never creates, renames,
6
+ moves or deletes a file, never edits `pix3project.yaml` and never builds**. When the human asks
7
+ for one of those, you do it — by writing files and running the CLI. The editor follows the disk
8
+ on its own. After every recipe: `pix3 check`, then (editor open) `pix3_sync` with `expect` =
9
+ the sha256 of every file you wrote, or (no editor) `pix3 smoke <the scene>`.
10
+
11
+ | The human asks for | Recipe |
12
+ | --- | --- |
13
+ | a new scene, a copy of a scene ("Save As") | [New scene](#new-scene) |
14
+ | a folder | create it; the asset panel shows it |
15
+ | a script | `export class X extends Script` in `scripts/X.ts` — `pix3-scripts` |
16
+ | an asset renamed, moved or deleted | [Rename, move, delete](#rename-move-delete) |
17
+ | a branch of a scene turned into a prefab ("Save Branch as Prefab") | [Extract a branch into a prefab](#extract-a-branch-into-a-prefab) |
18
+ | a flipbook for an `AnimatedSprite2D` | `pix3anim.md` → "Create one and bind it" |
19
+ | a language added or removed | [Add or remove a locale](#add-or-remove-a-locale) |
20
+ | project settings (resolution, entry scene, quality, fonts …) | [`pix3project.yaml`](#pix3projectyaml) |
21
+ | a build, an export | `npm run build` → `dist/` — [Build options](#build-options) |
22
+ | an image | write an SVG (AGENTS.md rule 5), or the human generates one in the editor's Generate panel and saves it into the project |
23
+
24
+ ## New scene
25
+
26
+ 1. Copy the entry scene (`defaultExportScenePath` in `pix3project.yaml`) to
27
+ `scenes/<name>.pix3scene` — it carries the root sized from `viewportBaseSize`. Keep the root
28
+ and what every screen has (a background), drop the root's `components` and the children that
29
+ are the other scene's game, and update `metadata.description` and the header comment. Ids
30
+ only have to be unique **within** a scene, so a copy keeps them.
31
+ 2. A scene under `scenes/` that is not under `scenes/prefabs/`, `prefabs/` or `scenes/ui/` is a
32
+ playable scene: reach it with `await this.scene.changeScene('res://scenes/<name>.pix3scene')`.
33
+ To make the game boot into it, set `defaultExportScenePath: scenes/<name>.pix3scene` — and
34
+ give it a way on (a menu's button script calls `changeScene` to the game).
35
+ 3. Under `scenes/prefabs/`, `prefabs/` or `scenes/ui/` the file is a prefab or an overlay: it is
36
+ instanced, never booted.
37
+
38
+ ## Rename, move, delete
39
+
40
+ Everything refers to files by path, and nothing rewrites those paths for you.
41
+
42
+ 1. Find every reference first, in every text file of the project (not `node_modules/`,
43
+ `.pix3/`, `dist/`, `.git/`, and not the kit's own docs in `.claude/`):
44
+ `grep -rn "sprites/coin.png" --exclude-dir={node_modules,.pix3,dist,.git,.claude} .` — for
45
+ a folder, search it with its trailing slash (`sprites/coin/`), which `sprites/coins/` does
46
+ not match.
47
+ — scenes and prefabs (`res://…`), `.pix3anim` frame paths, `locales/*.json` `sprites`,
48
+ scripts, and `pix3project.yaml` (which writes paths **without** `res://`:
49
+ `defaultExportScenePath`, `fonts[].path`). Search the folder name too: a path a script builds
50
+ at runtime (`` `res://levels/${n}.pix3scene` ``) has no literal to find.
51
+ 2. Move the file (`git mv` when git tracks it, else `mv`) and rewrite every reference; a folder
52
+ move rewrites the prefix. A flipbook folder keeps its `<name>/<name>.pix3anim` pairing:
53
+ renaming `sprites/coin/` to `sprites/coins/` renames `coin.pix3anim` to `coins.pix3anim` too.
54
+ To delete, first remove or replace the references, then the file.
55
+ 3. `pix3 check` names a reference that still points nowhere: a scene's (`E_MISSING_RESOURCE`,
56
+ `E_MISSING_PREFAB`), a `.pix3anim` frame (`E_MISSING_FRAME`) and a locale table's `sprites`
57
+ image (`E_MISSING_LOCALE_SPRITE`); `pix3 smoke` on a scene reports a missing file only when
58
+ it loads, as the **warning** `W_SMOKE_MISSING_RESOURCE` (exit 0).
59
+
60
+ A script's file name is free to change: `type: user:Mover` names the exported class. Renaming
61
+ the class means rewriting every `type: user:Mover` and every `import`.
62
+
63
+ ## Extract a branch into a prefab
64
+
65
+ The branch: a node of the host scene with everything under it, e.g. in `scenes/main.pix3scene`
66
+
67
+ ```yaml
68
+ - id: coin-hud
69
+ type: Group2D
70
+ name: Coin HUD
71
+ properties:
72
+ width: 260
73
+ height: 80
74
+ transform: { position: [0, 0], scale: [1, 1], rotation: 0 }
75
+ layout: { enabled: true, horizontalAlign: left, verticalAlign: top, left: 60, top: 40 }
76
+ children:
77
+ - id: coin-icon
78
+ type: ColorRect2D
79
+ name: Coin Icon
80
+ properties: { width: 64, height: 64, color: "#f5c542", transform: { position: [-80, 0] } }
81
+ - id: coin-count
82
+ type: Label2D
83
+ name: Coin Count
84
+ properties: { label: "0", labelFontSize: 48, transform: { position: [40, 0] } }
85
+ ```
86
+
87
+ 1. Sync and re-read the host scene (AGENTS.md rule 2).
88
+ 2. Write `scenes/prefabs/CoinHud.pix3scene`: `version: 1.0.0` and a `root:` with **exactly one**
89
+ node — the branch root, cut with everything under it (`properties`, `components`, `groups`,
90
+ `children`) and indented back to the top level.
91
+ 3. The prefab root keeps its size but not its placement: set its `transform` to
92
+ `{ position: [0, 0], scale: [1, 1], rotation: 0 }` and drop its `layout:` block. Where it
93
+ sits is the instance's business (step 5).
94
+ 4. Ids. Keep the branch's ids — the prefab root's `id` as it was, the inner ones unique in the
95
+ file. In the host scene the instance node's id becomes the root's id, and every inner node
96
+ keeps its prefab id **when that id is free in the host scene**; otherwise the loader renames
97
+ it `<id>-1`, `<id>-2`, … — which is what happens to the second instance of the same prefab.
98
+ So a lookup by id (`findNode('coin-count')`) finds the first instance's node only; to reach
99
+ an inner node of each instance, go through the instance — a second copy is one more instance
100
+ node with an id and name you choose (`coin-hud-2`, "Coin HUD 2"), its inner `coin-count`
101
+ becomes `coin-count-1`: `this.findNode('coin-hud-2')?.findByName('Coin Count')` (names never
102
+ change; the renamed id is its `nodeId`). A `node` property of a component inside the prefab that names another
103
+ node of the prefab is re-pointed per instance by the loader.
104
+ 5. Replace the branch in the host scene, at the same place in `children` (tree order is paint
105
+ order), with one instance node: the branch root's `id` (scripts that find it keep finding
106
+ it) and `name`, `instance: res://scenes/prefabs/CoinHud.pix3scene`, and `properties` with the
107
+ root's `transform` and its anchoring in the **flat** names an instance reads —
108
+ `layoutEnabled`, `horizontalAlign`, `verticalAlign` and the margins `layoutLeft` /
109
+ `layoutRight` / `layoutTop` / `layoutBottom` (scene-format "Prefab instance"). No `type`,
110
+ no `components`, no `children`:
111
+
112
+ ```yaml
113
+ - id: coin-hud
114
+ name: Coin HUD
115
+ instance: res://scenes/prefabs/CoinHud.pix3scene
116
+ properties:
117
+ transform: { position: [0, 0] }
118
+ layoutEnabled: true
119
+ horizontalAlign: left
120
+ verticalAlign: top
121
+ layoutLeft: 60
122
+ layoutTop: 40
123
+ ```
124
+
125
+ 6. A value that differed per copy goes into the instance: on the root as above, on an inner
126
+ node in `overrides.byLocalId` (`coin-count: { properties: { label: "10" } }`, keyed by the
127
+ prefab's ids). These are node **properties** and the values a copy starts with: a script
128
+ inside the prefab that sets the same property in `onStart` wins, and a component's `config`
129
+ cannot differ per copy (no component overrides) — make the script read the node property,
130
+ or keep the tunable on a plain node of the host scene. Components start parent first, so a
131
+ host script's `onStart` sees an inner node before the prefab's own scripts touched it. A
132
+ label with a `labelKey` shows that key's text, whatever `label` says: per copy, override the
133
+ `labelKey` (one key per copy) instead.
134
+ 7. `pix3 check` — `E_PREFAB_ROOT` (not one root), `E_DUPLICATE_ID`, `E_MISSING_PREFAB`,
135
+ `E_UNKNOWN_OVERRIDE_TARGET` name the slips — then sync with `expect` of both files and look
136
+ at it running.
137
+
138
+ ## Add or remove a locale
139
+
140
+ A locale is a table `locales/<id>.json` at the project root — `strings` (key → text, `{name}`
141
+ placeholders) and `sprites` (key → `res://` image for art with baked-in text). Nodes and
142
+ scripts name **keys** (`labelKey` on a UI control, `textureKey` on a `Sprite2D`,
143
+ `this.scene.localization.tr('menu.play')`), never a locale.
144
+
145
+ ```json
146
+ {
147
+ "$meta": { "locale": "de", "name": "Deutsch" },
148
+ "strings": { "menu.play": "Spielen" },
149
+ "sprites": {}
150
+ }
151
+ ```
152
+
153
+ Add one:
154
+
155
+ 0. No table yet: write the default one first — `locales/en.json` with the keys the game uses
156
+ (`labelKey` on the nodes; keep `label` as well, it is what shows while there is no table) —
157
+ and declare it alone (`locales: [en]`, step 2).
158
+ 1. Copy the default locale's table to `locales/<id>.json`, set `$meta.locale` and `$meta.name`
159
+ (`"direction": "rtl"` for a right-to-left language) and translate the values. Keep every key;
160
+ one you cannot translate yet stays `""` and falls back to the fallback locale, then to the key.
161
+ 2. Declare it in `pix3project.yaml`, creating the block if there is none:
162
+
163
+ ```yaml
164
+ localization:
165
+ defaultLocale: en
166
+ locales: [en, de]
167
+ ```
168
+
169
+ Without a block every `locales/*.json` is a locale (`en` the default when there is one);
170
+ with it, only the tables it names count — the build ships those and no other.
171
+
172
+ Remove one:
173
+
174
+ 1. Never the `defaultLocale` or the `fallbackLocale` — point those at another locale first.
175
+ 2. Drop the id from `localization.locales`, delete `locales/<id>.json`, and grep the scripts for
176
+ the id (`setLocale('de')`). An image only that table's `sprites` used is now unreferenced
177
+ (`pix3 check` lists those under `sprites/` as `W_UNUSED_ASSET`).
178
+
179
+ `pix3 check` checks the tables: a declared locale without its file (`E_LOCALE_MISSING` for the
180
+ default or fallback locale, `W_LOCALE_MISSING` for another), a table that is not JSON
181
+ (`E_`/`W_LOCALE_JSON`), a value that is not a string (`E_`/`W_LOCALE_VALUE` — keys are flat,
182
+ `"menu.play"`, never nested), a `sprites` image that does not exist (`E_MISSING_LOCALE_SPRITE`),
183
+ a `labelKey` the default locale has no text for (`E_LOCALE_KEY`: the node would show the key
184
+ itself), a key a script passes as a literal — `tr('menu.play')`, `setTextKey('…')`,
185
+ `trPlural('…')` — with no text (`E_LOCALE_SCRIPT_KEY`), and a `textureKey` /
186
+ `stateTextureKeys` / `trSprite('…')` key with no `sprites` entry (`E_`/`W_LOCALE_SPRITE_KEY`).
187
+ A key the script builds at run time (`` tr(`shop.${id}`) ``, `tr(key)`) is not checked — read
188
+ those back yourself (or look at the running game).
189
+
190
+ In a script, `this.scene.localization`: `tr(key, { name: value })` (fills `{name}`),
191
+ `trPlural(key, count)`, `trSprite(key)`, `has(key)`, `locale`, `locales`, `setLocale(id)`,
192
+ `onChange(fn)`; a node shows `tr(node.labelKey)` when it has a key.
193
+
194
+ ## `pix3project.yaml`
195
+
196
+ The manifest. Change a key only when the human asks for that setting (or a recipe above says
197
+ so), keep every key you do not know, and copy the style of the file.
198
+
199
+ | Key | What |
200
+ | --- | --- |
201
+ | `version` | `1.0.0`; leave it |
202
+ | `defaultExportScenePath` | the scene the game boots into (no `res://`) |
203
+ | `viewportBaseSize` | `{ width, height }` — the design resolution 2D layout is authored against |
204
+ | `projectType` | `2d` or `3d` |
205
+ | `targetPlatform` | `mobile`, `desktop` or `universal` |
206
+ | `textureFiltering` | `linear` (smooth) or `nearest` (pixel art) for 2D textures |
207
+ | `ambientOcclusion` | `off`, `baked`, `realtime` or `adaptive` — the project default |
208
+ | `quality` | `{ antialias, shadows, maxPixelRatio }` |
209
+ | `localization` | `{ defaultLocale, fallbackLocale?, locales }` — above |
210
+ | `fonts` | `[{ family, path, weight, style, unicodeRange? }]` — font files registered before the first frame; `path` without `res://` |
211
+ | `autoloads` | `[{ singleton, scriptPath, enabled }]` — game-wide script singletons that survive `changeScene` (recipe below); `[]` when there are none |
212
+ | `metadata` | the editor's and `pix3 kit`'s bookkeeping (`projectId`, `agentKit`) — do not edit |
213
+
214
+ ### An autoload (state or a service for the whole game)
215
+
216
+ State that must outlive a scene change (score across levels, settings, a music player) is an
217
+ autoload: one script instance per game session, built before the first scene starts, kept
218
+ across every `changeScene`, the same in the editor's play, `npm run dev` and the build.
219
+
220
+ 1. `scripts/GameState.ts`: `export class GameState extends Script { … }` — the class is named
221
+ like the file (that is how the autoload finds it).
222
+ 2. In `pix3project.yaml`:
223
+ ```yaml
224
+ autoloads:
225
+ - singleton: GameState
226
+ scriptPath: scripts/GameState.ts
227
+ ```
228
+ 3. Any script: `const state = this.scene.getAutoload(GameState)` (import the class) or
229
+ `this.scene.getAutoload<GameState>('GameState')`; null when it is not declared.
230
+
231
+ It runs `onStart`/`onUpdate` like a scene script, before the scene's every frame, and has no
232
+ node in any scene (it draws nothing; give the visuals to a scene). Commands it registers with
233
+ `scene.commands` belong to the scene and go with it. `pix3 check` reports an autoload that does
234
+ not resolve (`E_AUTOLOAD`).
235
+
236
+ ## Build options
237
+
238
+ `npm run build` writes `dist/index.html` (one self-contained file) and
239
+ `dist/<name>.report.json` (what it weighs and why). How it builds is `pix3()` in
240
+ `vite.config.ts`, not the manifest: `pix3({ build: 'zip' })` (a zip to host instead of one
241
+ file), `pix3({ compress: true })` (gzip inside the page, about two thirds smaller),
242
+ `pix3({ entryScene: 'scenes/level-1.pix3scene' })` (boot another scene than
243
+ `defaultExportScenePath`), `pix3({ strip: { keep: [...] } })` (when the build says a library
244
+ needs runtime classes it cannot see).