@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,132 +1,132 @@
1
- # Recipe: recipe-tapper-2d
2
-
3
- ## What this is
4
-
5
- Objects fall into a field; tapping them is the whole game. One "bad" type
6
- punishes instead of scoring. A countdown ends the run and a target score decides
7
- win or lose. Playable before any edit.
8
-
9
- Same script names and roles as the other recipes — `Spawner`, `TouchRules`,
10
- `ScoreHud`, `GameRules` — so what you learn here transfers.
11
-
12
- ## Node map
13
-
14
- | id | what |
15
- | --- | --- |
16
- | `game-root` | root; hosts `GameRules` + `TouchRules`; all HUD signals emitted here |
17
- | `game-background` | full-screen `ColorRect2D` (palette background) |
18
- | `board` | the field (`Group2D`) |
19
- | `board-floor` | the field's visible plate |
20
- | `spawner-targets` | spawn band for tappable things; hosts `Spawner` |
21
- | `spawner-hazards` | spawn band for the bad type; hosts `Spawner` |
22
- | `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
23
- | `score-label` / `time-label` / `lives-bar` / `menu-button` | HUD |
24
- | `result-overlay` / `result-label` / `retry-button` | end screen in `scenes/ui/result.pix3scene`, instanced into main hidden (`visible: false` is editor-only) |
25
-
26
- Signals: `TouchRules` emits `touch-scored` / `touch-damaged` on `game-root`;
27
- `GameRules` answers `score-changed`, `lives-changed`, `time-changed`,
28
- `game-won` / `game-lost`.
29
-
30
- ## Placeholders
31
-
32
- Near-white PNGs carrying a `core:tint` effect: set `effects[0].params.color` and
33
- the multiply gives you that palette colour.
34
-
35
- | role | file | node/prefab |
36
- | --- | --- | --- |
37
- | collectible | `sprites/ph-target.png` | `prefabs/target.pix3scene` |
38
- | threat | `sprites/ph-hazard.png` | `prefabs/hazard.pix3scene` |
39
-
40
- ## Tunables
41
-
42
- ```yaml
43
- tunables:
44
- targetRate: { node: spawner-targets, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 0.85 }
45
- targetMaxAlive: { node: spawner-targets, component: "user:Spawner", property: maxAlive, min: 1, max: 200, default: 8 }
46
- targetFallSpeed: { node: spawner-targets, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -230 }
47
- targetLifetime: { node: spawner-targets, component: "user:Spawner", property: lifetimeSec, min: 0, max: 120, default: 8 }
48
- hazardRate: { node: spawner-hazards, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 2.2 }
49
- hazardFallSpeed: { node: spawner-hazards, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -260 }
50
- tapRadius: { node: game-root, component: "user:TouchRules", property: tapRadius, min: 0, max: 400, default: 40 }
51
- missPenalty: { node: game-root, component: "user:TouchRules", property: missPenalty, min: 0, max: 10, default: 0 }
52
- touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "target:score:1;hazard:damage:1" }
53
- winMode: { node: game-root, component: "user:GameRules", property: winMode, default: time }
54
- targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 18 }
55
- timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 30 }
56
- startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
57
- bgColor: { node: game-background, property: color, default: "#141a2e" }
58
- boardColor: { node: board-floor, property: color, default: "#1d2743" }
59
- ```
60
-
61
- `component` present → `set_component_property`; absent → `set_property`. The
62
- schema clamps out-of-range values rather than rejecting them.
63
-
64
- ## Extension points
65
-
66
- - **Popping instead of falling (whack-a-mole).** Set `targetFallSpeed: 0` and
67
- `targetLifetime` to ~1.2 s: things appear in place and vanish if not tapped.
68
- Set `gridSnap` on the spawner (e.g. 220) and raise `spawnHeight` to get a mole
69
- grid instead of a band.
70
- - **Combos / multipliers.** A new script on `hud` that listens for
71
- `touch-scored` on `game-root`, counts hits inside a time window, and adds
72
- only the **extra** points: `game-root.emit('touch-scored', amount * (m - 1), x, y)`
73
- under a re-entrancy guard (`if (this.emitting) return; this.emitting = true;
74
- try { emit(...) } finally { this.emitting = false; }`). `GameRules` already
75
- counted the base `amount` and needs no change. Show the multiplier through a
76
- signal of its own (`combo-changed`), never through `touch-scored`.
77
- **The trap:** a listener on `touch-scored` that emits `touch-scored` on the
78
- same node is called again by its own emit — without the guard, the first tap
79
- recurses until the stack overflows (and re-emitting the *full* amount would
80
- double-count the base). No offline check sees it: `pix3 check` and
81
- `pix3 smoke` are green, because smoke taps nothing. Say so, and have the human
82
- tap three targets in a row.
83
- - **A third type.** Copy a prefab, give its `core:Hitbox2D` a new group, add a
84
- node with a third `Spawner` (`create_node` + `add_component`), and append
85
- `<group>:score:5` to `touchRules`.
86
- - **Punish misses.** Raise `missPenalty` — a tap that hits nothing then costs a
87
- life, which turns a relaxed collector into a precision game.
88
- - **Sudden death.** `startingLives: 1`, or `winMode: survive` with a longer
89
- `timeLimitSec` so surviving the clock is the win.
90
-
91
- Replacing a whole script the list above names is expected: use `fs_write` with
92
- `overwrite: true` and a short `reason` (the size guard only blocks *silent*
93
- wholesale rewrites).
94
-
95
- ## Ending a run belongs to GameRules
96
-
97
- The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
98
- start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
99
- writes the result text, enables RETRY and freezes the `freezeNodes` subtrees
100
- (every component on them and on what they spawned). For a custom
101
- win/lose condition call `finish(true|false)` on that component — do NOT show the
102
- overlay yourself. A hand-rolled ending leaves RETRY on screen with its handler bound
103
- and `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
104
- `control: { enabled: false }`; that is the tell).
105
-
106
- ## Do not touch
107
-
108
- - The node ids above (rename `name`, never `id`), and the signal names.
109
- - Component config on a **prefab instance** — the editor locks it. Prefabs carry
110
- fixed defaults only (their hitbox group); tunables live on plain nodes in
111
- `main.pix3scene`.
112
- - The prefab hitbox groups must stay in sync with `touchRules`; the group is the
113
- only thing that separates "good" from "bad".
114
-
115
- ## Playtest contract
116
-
117
- terminalVisual: static
118
- terminalRestartRoutine: terminal-retry
119
-
120
- After the result overlay appears, the screen should remain still until RETRY. The
121
- playtest samples five rendered frames at 15-frame intervals after the terminal
122
- phase has held for 120 frames. The `terminal-retry` routine physically taps RETRY
123
- and checks that the game returns to its playing phase.
124
-
125
- ## Verify
126
-
127
- 1. `play_start` on `scenes/main.pix3scene`.
128
- 2. After ~2 s, children exist under `spawner-targets` and are falling.
129
- 3. `game_input` a tap on one of them → it disappears, `score-label` climbs, and
130
- the punch-scale fires.
131
- 4. Tap three hazards → `lives-bar` empties and `result-overlay` shows
132
- `GAME OVER`; `retry-button` restarts the scene.
1
+ # Recipe: recipe-tapper-2d
2
+
3
+ ## What this is
4
+
5
+ Objects fall into a field; tapping them is the whole game. One "bad" type
6
+ punishes instead of scoring. A countdown ends the run and a target score decides
7
+ win or lose. Playable before any edit.
8
+
9
+ Same script names and roles as the other recipes — `Spawner`, `TouchRules`,
10
+ `ScoreHud`, `GameRules` — so what you learn here transfers.
11
+
12
+ ## Node map
13
+
14
+ | id | what |
15
+ | --- | --- |
16
+ | `game-root` | root; hosts `GameRules` + `TouchRules`; all HUD signals emitted here |
17
+ | `game-background` | full-screen `ColorRect2D` (palette background) |
18
+ | `board` | the field (`Group2D`) |
19
+ | `board-floor` | the field's visible plate |
20
+ | `spawner-targets` | spawn band for tappable things; hosts `Spawner` |
21
+ | `spawner-hazards` | spawn band for the bad type; hosts `Spawner` |
22
+ | `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
23
+ | `score-label` / `time-label` / `lives-bar` / `menu-button` | HUD |
24
+ | `result-overlay` / `result-label` / `retry-button` | end screen in `scenes/ui/result.pix3scene`, instanced into main hidden (`visible: false` is editor-only) |
25
+
26
+ Signals: `TouchRules` emits `touch-scored` / `touch-damaged` on `game-root`;
27
+ `GameRules` answers `score-changed`, `lives-changed`, `time-changed`,
28
+ `game-won` / `game-lost`.
29
+
30
+ ## Placeholders
31
+
32
+ Near-white PNGs carrying a `core:tint` effect: set `effects[0].params.color` and
33
+ the multiply gives you that palette colour.
34
+
35
+ | role | file | node/prefab |
36
+ | --- | --- | --- |
37
+ | collectible | `sprites/ph-target.png` | `prefabs/target.pix3scene` |
38
+ | threat | `sprites/ph-hazard.png` | `prefabs/hazard.pix3scene` |
39
+
40
+ ## Tunables
41
+
42
+ ```yaml
43
+ tunables:
44
+ targetRate: { node: spawner-targets, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 0.85 }
45
+ targetMaxAlive: { node: spawner-targets, component: "user:Spawner", property: maxAlive, min: 1, max: 200, default: 8 }
46
+ targetFallSpeed: { node: spawner-targets, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -230 }
47
+ targetLifetime: { node: spawner-targets, component: "user:Spawner", property: lifetimeSec, min: 0, max: 120, default: 8 }
48
+ hazardRate: { node: spawner-hazards, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 2.2 }
49
+ hazardFallSpeed: { node: spawner-hazards, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -260 }
50
+ tapRadius: { node: game-root, component: "user:TouchRules", property: tapRadius, min: 0, max: 400, default: 40 }
51
+ missPenalty: { node: game-root, component: "user:TouchRules", property: missPenalty, min: 0, max: 10, default: 0 }
52
+ touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "target:score:1;hazard:damage:1" }
53
+ winMode: { node: game-root, component: "user:GameRules", property: winMode, default: time }
54
+ targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 18 }
55
+ timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 30 }
56
+ startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
57
+ bgColor: { node: game-background, property: color, default: "#141a2e" }
58
+ boardColor: { node: board-floor, property: color, default: "#1d2743" }
59
+ ```
60
+
61
+ `component` present → `set_component_property`; absent → `set_property`. The
62
+ schema clamps out-of-range values rather than rejecting them.
63
+
64
+ ## Extension points
65
+
66
+ - **Popping instead of falling (whack-a-mole).** Set `targetFallSpeed: 0` and
67
+ `targetLifetime` to ~1.2 s: things appear in place and vanish if not tapped.
68
+ Set `gridSnap` on the spawner (e.g. 220) and raise `spawnHeight` to get a mole
69
+ grid instead of a band.
70
+ - **Combos / multipliers.** A new script on `hud` that listens for
71
+ `touch-scored` on `game-root`, counts hits inside a time window, and adds
72
+ only the **extra** points: `game-root.emit('touch-scored', amount * (m - 1), x, y)`
73
+ under a re-entrancy guard (`if (this.emitting) return; this.emitting = true;
74
+ try { emit(...) } finally { this.emitting = false; }`). `GameRules` already
75
+ counted the base `amount` and needs no change. Show the multiplier through a
76
+ signal of its own (`combo-changed`), never through `touch-scored`.
77
+ **The trap:** a listener on `touch-scored` that emits `touch-scored` on the
78
+ same node is called again by its own emit — without the guard, the first tap
79
+ recurses until the stack overflows (and re-emitting the *full* amount would
80
+ double-count the base). No offline check sees it: `pix3 check` and
81
+ `pix3 smoke` are green, because smoke taps nothing. Say so, and have the human
82
+ tap three targets in a row.
83
+ - **A third type.** Copy a prefab, give its `core:Hitbox2D` a new group, add a
84
+ node with a third `Spawner` (`create_node` + `add_component`), and append
85
+ `<group>:score:5` to `touchRules`.
86
+ - **Punish misses.** Raise `missPenalty` — a tap that hits nothing then costs a
87
+ life, which turns a relaxed collector into a precision game.
88
+ - **Sudden death.** `startingLives: 1`, or `winMode: survive` with a longer
89
+ `timeLimitSec` so surviving the clock is the win.
90
+
91
+ Replacing a whole script the list above names is expected: use `fs_write` with
92
+ `overwrite: true` and a short `reason` (the size guard only blocks *silent*
93
+ wholesale rewrites).
94
+
95
+ ## Ending a run belongs to GameRules
96
+
97
+ The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
98
+ start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
99
+ writes the result text, enables RETRY and freezes the `freezeNodes` subtrees
100
+ (every component on them and on what they spawned). For a custom
101
+ win/lose condition call `finish(true|false)` on that component — do NOT show the
102
+ overlay yourself. A hand-rolled ending leaves RETRY on screen with its handler bound
103
+ and `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
104
+ `control: { enabled: false }`; that is the tell).
105
+
106
+ ## Do not touch
107
+
108
+ - The node ids above (rename `name`, never `id`), and the signal names.
109
+ - Component config on a **prefab instance** — the editor locks it. Prefabs carry
110
+ fixed defaults only (their hitbox group); tunables live on plain nodes in
111
+ `main.pix3scene`.
112
+ - The prefab hitbox groups must stay in sync with `touchRules`; the group is the
113
+ only thing that separates "good" from "bad".
114
+
115
+ ## Playtest contract
116
+
117
+ terminalVisual: static
118
+ terminalRestartRoutine: terminal-retry
119
+
120
+ After the result overlay appears, the screen should remain still until RETRY. The
121
+ playtest samples five rendered frames at 15-frame intervals after the terminal
122
+ phase has held for 120 frames. The `terminal-retry` routine physically taps RETRY
123
+ and checks that the game returns to its playing phase.
124
+
125
+ ## Verify
126
+
127
+ 1. `play_start` on `scenes/main.pix3scene`.
128
+ 2. After ~2 s, children exist under `spawner-targets` and are falling.
129
+ 3. `game_input` a tap on one of them → it disappears, `score-label` climbs, and
130
+ the punch-scale fires.
131
+ 4. Tap three hazards → `lives-bar` empties and `result-overlay` shows
132
+ `GAME OVER`; `retry-button` restarts the scene.
@@ -1,4 +1,4 @@
1
- {
2
- "version": 1,
3
- "proven": []
4
- }
1
+ {
2
+ "version": 1,
3
+ "proven": []
4
+ }
@@ -1,23 +1,23 @@
1
- {
2
- "name": "terminal-retry",
3
- "description": "From the result screen, physically tap RETRY and check that play resumes.",
4
- "scope": "scenes/main.pix3scene",
5
- "params": {},
6
- "uses": ["result-overlay", "retry-button"],
7
- "steps": [
8
- {
9
- "type": "tap",
10
- "target": "retry-button",
11
- "holdMs": 200,
12
- "why": "A real pointer press proves the button is visible, enabled and reachable."
13
- },
14
- {
15
- "type": "wait",
16
- "frames": 30,
17
- "why": "Let the scene transition finish before reading the new game's phase."
18
- }
19
- ],
20
- "expect": [
21
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" }
22
- ]
23
- }
1
+ {
2
+ "name": "terminal-retry",
3
+ "description": "From the result screen, physically tap RETRY and check that play resumes.",
4
+ "scope": "scenes/main.pix3scene",
5
+ "params": {},
6
+ "uses": ["result-overlay", "retry-button"],
7
+ "steps": [
8
+ {
9
+ "type": "tap",
10
+ "target": "retry-button",
11
+ "holdMs": 200,
12
+ "why": "A real pointer press proves the button is visible, enabled and reachable."
13
+ },
14
+ {
15
+ "type": "wait",
16
+ "frames": 30,
17
+ "why": "Let the scene transition finish before reading the new game's phase."
18
+ }
19
+ ],
20
+ "expect": [
21
+ { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" }
22
+ ]
23
+ }