@pix3/cli 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-7MZGPAUK.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-7WOFGLW3.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-HKG6YI6Y.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +32 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
@@ -1,148 +1,148 @@
1
- # Recipe: recipe-blank-2d
2
-
3
- ## What this is
4
-
5
- **Nothing plays yet, on purpose.** A bounded field, a HUD, and score / lives /
6
- timer / win-lose bookkeeping that already works — and no mechanic of any kind,
7
- including controls.
8
-
9
- Pick this recipe when the idea's core loop is not what another recipe ships:
10
- grid or turn-based movement (snake, sokoban, match-3), word / card / board games,
11
- builders, idle games without a ball. **Your first increment is the core
12
- mechanic itself, controls included** — that is the trade being made here, and it
13
- beats spending the first increment deleting somebody else's mechanic.
14
-
15
- Everything *around* the mechanic is done: you never hand-roll a score counter, a
16
- lives bar, a timer, an end screen or a retry. The look is done too: `post-fx`
17
- blooms anything bright and `bg-glow` lights the field, so a sprite tinted with a
18
- palette accent already glows on the first frame.
19
-
20
- ## Node map
21
-
22
- | id | what |
23
- | --- | --- |
24
- | `post-fx` | `PostProcess` (`affect2D`): bloom + vignette — tune, never delete |
25
- | `game-root` | root; hosts `GameRules`; every HUD signal is emitted here |
26
- | `game-background` | full-screen `ColorRect2D` (palette background) |
27
- | `bg-glow` | full-screen `Sprite2D`, the tinted radial gradient over it |
28
- | `board` | the play field (`Group2D`) — **build your mechanic inside this** |
29
- | `board-floor` | the field's visible plate; its rect is the field's extent |
30
- | `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
31
- | `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
32
- | `result-overlay` / `result-dim` / `result-label` / `retry-button` | end screen, own file `scenes/ui/result.pix3scene`; instanced into main with `visible: false` (editor-only hide — `GameRules` shows it) |
33
-
34
- Signals on `game-root`: your mechanic emits `score-added` (amount) and
35
- `life-lost` (amount); `GameRules` answers `score-changed`, `lives-changed`,
36
- `time-changed`, and `game-won` / `game-lost`.
37
-
38
- There is **one gameplay scene** (`scenes/main.pix3scene`) and no menu — see
39
- Extension points if the game eventually needs one.
40
-
41
- ## Placeholders
42
-
43
- A **shape library**, not entities: near-white PNGs already tinted with the
44
- palette at T0 (the role column decides the colour). Put them on a `Sprite2D`
45
- sized to the thing they stand for, and recolour with `effects: [{type: core:tint,
46
- params: {color}}]` when you need another accent. **A round thing is
47
- `ph-circle`/`ph-orb`, a coin or bumper is `ph-ring`, a bar or paddle is
48
- `ph-capsule`, a spark or star is `ph-star`.** A `ColorRect2D` is for panels,
49
- floors and bars — never for a ball, a coin, an enemy or anything the player
50
- looks at: a square ball on the first frame is what the user remembers. Real art
51
- replaces these through `generate_asset` in the art pass.
52
-
53
- | role | file | node/prefab |
54
- | --- | --- | --- |
55
- | background | `sprites/ph-bg.png` | `bg-glow` (radial gradient) |
56
- | player | `sprites/ph-circle.png` | (library — a solid disc with a soft edge) |
57
- | avatar | `sprites/ph-orb.png` | (library — a disc with a bright rim) |
58
- | collectible | `sprites/ph-ring.png` | (library — a ring; bumpers, coins, targets) |
59
- | hazard | `sprites/ph-star.png` | (library — a four-point spark) |
60
- | ui | `sprites/ph-capsule.png` | (library — a rounded bar; paddles, platforms, pills) |
61
-
62
- ## Tunables
63
-
64
- ```yaml
65
- tunables:
66
- winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
67
- targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
68
- timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
69
- startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
70
- bgColor: { node: game-background, property: color, default: "#12141c" }
71
- boardColor: { node: board-floor, property: color, default: "#1d212e" }
72
- bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.8 }
73
- ```
74
-
75
- `component` present → `set_component_property`; absent → `set_property`. The
76
- schema clamps out-of-range values rather than rejecting them.
77
-
78
- `timeLimitSec: 0` means **no clock**: the run ends on the target score or on
79
- running out of lives, and `time-label` counts elapsed seconds up. Set a limit and
80
- it counts down and becomes a deadline (see `winMode`).
81
-
82
- ## Extension points
83
-
84
- - **The mechanic.** Write a script under `scripts/`, attach it to a node inside
85
- `board`, and let it own input and movement. It scores through `GameRules` —
86
- either `node.emit('score-added', 1)` on `game-root`, or a direct
87
- `addScore(1)` / `loseLife()` / `finish(true)` on the component. Nothing else in
88
- the project needs to change for the HUD and the end screen to work.
89
- - **Collision, when the mechanic needs it.** Three tiers, cheapest first. Overlap
90
- queries with no response: `scene.collision2d` + `core:Hitbox2D` (axis-aligned).
91
- Bouncing, pushing, gravity, sensors: the engine's own 2D rigid-body solver —
92
- `core:PhysicsBody2D` + `core:Collider2D` on the same node (a `core:Collider2D`
93
- alone is static world geometry), `scene.physics2d.getBody(node)` for
94
- `applyImpulse` / `setVelocity` / `teleport`, `setGravity(0, 0)` for top-down.
95
- **Never hand-write a 2D solver, and do not import rapier** (3D only, ~2 MB wasm).
96
- Grid games usually need no collision at all — compare cell coordinates.
97
- - **Spawning.** `scene.instantiate` a prefab into a container node, `queueFree`
98
- it when it leaves the field, and add that container to `GameRules.freezeNodes`
99
- so it — and every instance under it — stops on game over. Give your spawner a `clear()` method and `resetRun()`
100
- will empty the field for you.
101
- - **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
102
- `scene.juice.burst({x, y})`, `floatText('+5', {at: node})`, `punchScale` /
103
- `shake` / `flash`, `scene.audio.sfx('score')`, `scene.time.hitstop(50)` on a
104
- contact START; `scene.juice.trail(ball)` for a light streak; `scene.tween.to(node,
105
- {scale: 1.2}, {durationSec: 0.15, yoyo: true, repeat: 1})` / `crossFade(a, b)` for any
106
- motion or panel switch instead of hand-lerping. Bright sprites bloom through `post-fx`
107
- by themselves.
108
- - **A menu, later.** Create `scenes/menu.pix3scene` with a root carrying a script
109
- that calls `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`
110
- on PLAY, then set Project Settings → Default Export Scene Path to it. Do this
111
- **after** the game is fun, never before: while iterating, the menu is a screen
112
- between you and the thing you are working on.
113
-
114
- ## Ending a run belongs to GameRules
115
-
116
- The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
117
- start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
118
- writes the result text, enables RETRY and freezes the `freezeNodes` nodes. For a
119
- custom win/lose condition call `finish(true|false)` — do NOT show the overlay
120
- yourself. A hand-rolled ending leaves RETRY on screen with its handler bound and
121
- `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
122
- `control: { enabled: false }`; that is the tell).
123
-
124
- ## Do not touch
125
-
126
- - The node ids above (rename `name`, never `id`), and the signal names.
127
- - `GameRules`' ownership of `result-overlay` and `retry-button`.
128
- - `post-fx`: keep the id and `affect2D: true` — without it a 2D scene gets no
129
- bloom. Tune the values, don't delete the node.
130
- - Do not import rapier, and do not add a menu scene early.
131
-
132
- ## Playtest contract
133
-
134
- terminalVisual: static
135
- terminalRestartRoutine: terminal-retry
136
-
137
- ## Verify
138
-
139
- 1. `play_start` on `scenes/main.pix3scene` — `game_observe` reports the snapshot
140
- with `phase: playing`, `score: 0`, `lives: 3`. That is the baseline, before any
141
- mechanic exists.
142
- 2. After the first mechanic increment, prove it by **state delta**, not by a
143
- screenshot: `game_input` the real input with `observe:` on the node that should
144
- react, and read `moved` / `activity` / the `game.changed` diff.
145
- 3. Scoring is proven the same way: one real input, then `score` up in the snapshot
146
- (and `score-label` reading `SCORE 1`).
147
- 4. `targetScore: 1`, replay, score once → `result-overlay` visible with `YOU WIN!`;
148
- `retry-button` (or the `restart` intent) starts a fresh run.
1
+ # Recipe: recipe-blank-2d
2
+
3
+ ## What this is
4
+
5
+ **Nothing plays yet, on purpose.** A bounded field, a HUD, and score / lives /
6
+ timer / win-lose bookkeeping that already works — and no mechanic of any kind,
7
+ including controls.
8
+
9
+ Pick this recipe when the idea's core loop is not what another recipe ships:
10
+ grid or turn-based movement (snake, sokoban, match-3), word / card / board games,
11
+ builders, idle games without a ball. **Your first increment is the core
12
+ mechanic itself, controls included** — that is the trade being made here, and it
13
+ beats spending the first increment deleting somebody else's mechanic.
14
+
15
+ Everything *around* the mechanic is done: you never hand-roll a score counter, a
16
+ lives bar, a timer, an end screen or a retry. The look is done too: `post-fx`
17
+ blooms anything bright and `bg-glow` lights the field, so a sprite tinted with a
18
+ palette accent already glows on the first frame.
19
+
20
+ ## Node map
21
+
22
+ | id | what |
23
+ | --- | --- |
24
+ | `post-fx` | `PostProcess` (`affect2D`): bloom + vignette — tune, never delete |
25
+ | `game-root` | root; hosts `GameRules`; every HUD signal is emitted here |
26
+ | `game-background` | full-screen `ColorRect2D` (palette background) |
27
+ | `bg-glow` | full-screen `Sprite2D`, the tinted radial gradient over it |
28
+ | `board` | the play field (`Group2D`) — **build your mechanic inside this** |
29
+ | `board-floor` | the field's visible plate; its rect is the field's extent |
30
+ | `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
31
+ | `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
32
+ | `result-overlay` / `result-dim` / `result-label` / `retry-button` | end screen, own file `scenes/ui/result.pix3scene`; instanced into main with `visible: false` (editor-only hide — `GameRules` shows it) |
33
+
34
+ Signals on `game-root`: your mechanic emits `score-added` (amount) and
35
+ `life-lost` (amount); `GameRules` answers `score-changed`, `lives-changed`,
36
+ `time-changed`, and `game-won` / `game-lost`.
37
+
38
+ There is **one gameplay scene** (`scenes/main.pix3scene`) and no menu — see
39
+ Extension points if the game eventually needs one.
40
+
41
+ ## Placeholders
42
+
43
+ A **shape library**, not entities: near-white PNGs already tinted with the
44
+ palette at T0 (the role column decides the colour). Put them on a `Sprite2D`
45
+ sized to the thing they stand for, and recolour with `effects: [{type: core:tint,
46
+ params: {color}}]` when you need another accent. **A round thing is
47
+ `ph-circle`/`ph-orb`, a coin or bumper is `ph-ring`, a bar or paddle is
48
+ `ph-capsule`, a spark or star is `ph-star`.** A `ColorRect2D` is for panels,
49
+ floors and bars — never for a ball, a coin, an enemy or anything the player
50
+ looks at: a square ball on the first frame is what the user remembers. Real art
51
+ replaces these through `generate_asset` in the art pass.
52
+
53
+ | role | file | node/prefab |
54
+ | --- | --- | --- |
55
+ | background | `sprites/ph-bg.png` | `bg-glow` (radial gradient) |
56
+ | player | `sprites/ph-circle.png` | (library — a solid disc with a soft edge) |
57
+ | avatar | `sprites/ph-orb.png` | (library — a disc with a bright rim) |
58
+ | collectible | `sprites/ph-ring.png` | (library — a ring; bumpers, coins, targets) |
59
+ | hazard | `sprites/ph-star.png` | (library — a four-point spark) |
60
+ | ui | `sprites/ph-capsule.png` | (library — a rounded bar; paddles, platforms, pills) |
61
+
62
+ ## Tunables
63
+
64
+ ```yaml
65
+ tunables:
66
+ winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
67
+ targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
68
+ timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
69
+ startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
70
+ bgColor: { node: game-background, property: color, default: "#12141c" }
71
+ boardColor: { node: board-floor, property: color, default: "#1d212e" }
72
+ bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.8 }
73
+ ```
74
+
75
+ `component` present → `set_component_property`; absent → `set_property`. The
76
+ schema clamps out-of-range values rather than rejecting them.
77
+
78
+ `timeLimitSec: 0` means **no clock**: the run ends on the target score or on
79
+ running out of lives, and `time-label` counts elapsed seconds up. Set a limit and
80
+ it counts down and becomes a deadline (see `winMode`).
81
+
82
+ ## Extension points
83
+
84
+ - **The mechanic.** Write a script under `scripts/`, attach it to a node inside
85
+ `board`, and let it own input and movement. It scores through `GameRules` —
86
+ either `node.emit('score-added', 1)` on `game-root`, or a direct
87
+ `addScore(1)` / `loseLife()` / `finish(true)` on the component. Nothing else in
88
+ the project needs to change for the HUD and the end screen to work.
89
+ - **Collision, when the mechanic needs it.** Three tiers, cheapest first. Overlap
90
+ queries with no response: `scene.collision2d` + `core:Hitbox2D` (axis-aligned).
91
+ Bouncing, pushing, gravity, sensors: the engine's own 2D rigid-body solver —
92
+ `core:PhysicsBody2D` + `core:Collider2D` on the same node (a `core:Collider2D`
93
+ alone is static world geometry), `scene.physics2d.getBody(node)` for
94
+ `applyImpulse` / `setVelocity` / `teleport`, `setGravity(0, 0)` for top-down.
95
+ **Never hand-write a 2D solver, and do not import rapier** (3D only, ~2 MB wasm).
96
+ Grid games usually need no collision at all — compare cell coordinates.
97
+ - **Spawning.** `scene.instantiate` a prefab into a container node, `queueFree`
98
+ it when it leaves the field, and add that container to `GameRules.freezeNodes`
99
+ so it — and every instance under it — stops on game over. Give your spawner a `clear()` method and `resetRun()`
100
+ will empty the field for you.
101
+ - **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
102
+ `scene.juice.burst({x, y})`, `floatText('+5', {at: node})`, `punchScale` /
103
+ `shake` / `flash`, `scene.audio.sfx('score')`, `scene.time.hitstop(50)` on a
104
+ contact START; `scene.juice.trail(ball)` for a light streak; `scene.tween.to(node,
105
+ {scale: 1.2}, {durationSec: 0.15, yoyo: true, repeat: 1})` / `crossFade(a, b)` for any
106
+ motion or panel switch instead of hand-lerping. Bright sprites bloom through `post-fx`
107
+ by themselves.
108
+ - **A menu, later.** Create `scenes/menu.pix3scene` with a root carrying a script
109
+ that calls `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`
110
+ on PLAY, then set Project Settings → Default Export Scene Path to it. Do this
111
+ **after** the game is fun, never before: while iterating, the menu is a screen
112
+ between you and the thing you are working on.
113
+
114
+ ## Ending a run belongs to GameRules
115
+
116
+ The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
117
+ start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
118
+ writes the result text, enables RETRY and freezes the `freezeNodes` nodes. For a
119
+ custom win/lose condition call `finish(true|false)` — do NOT show the overlay
120
+ yourself. A hand-rolled ending leaves RETRY on screen with its handler bound and
121
+ `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
122
+ `control: { enabled: false }`; that is the tell).
123
+
124
+ ## Do not touch
125
+
126
+ - The node ids above (rename `name`, never `id`), and the signal names.
127
+ - `GameRules`' ownership of `result-overlay` and `retry-button`.
128
+ - `post-fx`: keep the id and `affect2D: true` — without it a 2D scene gets no
129
+ bloom. Tune the values, don't delete the node.
130
+ - Do not import rapier, and do not add a menu scene early.
131
+
132
+ ## Playtest contract
133
+
134
+ terminalVisual: static
135
+ terminalRestartRoutine: terminal-retry
136
+
137
+ ## Verify
138
+
139
+ 1. `play_start` on `scenes/main.pix3scene` — `game_observe` reports the snapshot
140
+ with `phase: playing`, `score: 0`, `lives: 3`. That is the baseline, before any
141
+ mechanic exists.
142
+ 2. After the first mechanic increment, prove it by **state delta**, not by a
143
+ screenshot: `game_input` the real input with `observe:` on the node that should
144
+ react, and read `moved` / `activity` / the `game.changed` diff.
145
+ 3. Scoring is proven the same way: one real input, then `score` up in the snapshot
146
+ (and `score-label` reading `SCORE 1`).
147
+ 4. `targetScore: 1`, replay, score once → `result-overlay` visible with `YOU WIN!`;
148
+ `retry-button` (or the `restart` intent) starts a fresh run.
@@ -1,4 +1,4 @@
1
- {
2
- "version": 1,
3
- "proven": []
4
- }
1
+ {
2
+ "version": 1,
3
+ "proven": []
4
+ }
@@ -1,27 +1,27 @@
1
- {
2
- "name": "restart",
3
- "description": "Dispatch the run's `restart` intent and confirm a fresh run came back up.",
4
- "note": "This is a FORMAT SEED, not a real test of your game — there is no mechanic here yet to test. Keep it (restart stays worth proving after a scene reload) and add a routine per mechanic next to it: run one with game_run {routine:'<name>'}. Steps are the game_input vocabulary plus {type:'command'} for a registered intent; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step against the frame pair around the run. Only a STEP takes a `why`. Prefer a `gameState` predicate over a node one wherever GameRules already publishes the value — it survives every relayout of the HUD.",
5
- "scope": "scenes/main.pix3scene",
6
- "params": {},
7
- "uses": ["game-root", "result-overlay", "retry-button"],
8
- "steps": [
9
- {
10
- "type": "command",
11
- "name": "restart",
12
- "why": "The intent RETRY dispatches, taken at the highest channel: it proves the wire without depending on where the button sits, and the dispatch is journalled with the frame it happened on."
13
- }
14
- ],
15
- "expect": [
16
- { "kind": "command", "name": "restart" },
17
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" },
18
- { "kind": "gameState", "path": "score", "op": "eq", "value": 0 },
19
- {
20
- "kind": "nodeProperty",
21
- "name": "result-overlay",
22
- "path": "visible",
23
- "op": "eq",
24
- "value": false
25
- }
26
- ]
27
- }
1
+ {
2
+ "name": "restart",
3
+ "description": "Dispatch the run's `restart` intent and confirm a fresh run came back up.",
4
+ "note": "This is a FORMAT SEED, not a real test of your game — there is no mechanic here yet to test. Keep it (restart stays worth proving after a scene reload) and add a routine per mechanic next to it: run one with game_run {routine:'<name>'}. Steps are the game_input vocabulary plus {type:'command'} for a registered intent; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step against the frame pair around the run. Only a STEP takes a `why`. Prefer a `gameState` predicate over a node one wherever GameRules already publishes the value — it survives every relayout of the HUD.",
5
+ "scope": "scenes/main.pix3scene",
6
+ "params": {},
7
+ "uses": ["game-root", "result-overlay", "retry-button"],
8
+ "steps": [
9
+ {
10
+ "type": "command",
11
+ "name": "restart",
12
+ "why": "The intent RETRY dispatches, taken at the highest channel: it proves the wire without depending on where the button sits, and the dispatch is journalled with the frame it happened on."
13
+ }
14
+ ],
15
+ "expect": [
16
+ { "kind": "command", "name": "restart" },
17
+ { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" },
18
+ { "kind": "gameState", "path": "score", "op": "eq", "value": 0 },
19
+ {
20
+ "kind": "nodeProperty",
21
+ "name": "result-overlay",
22
+ "path": "visible",
23
+ "op": "eq",
24
+ "value": false
25
+ }
26
+ ]
27
+ }