@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,127 +1,127 @@
1
- # Recipe: recipe-scene-3d
2
-
3
- ## What this is
4
-
5
- The only **3D** recipe: a portrait scene with a real 3D stage — perspective
6
- `Camera3D`, a key light and an ambient fill, a ground slab and one hero solid —
7
- under a 2D UI layer with a tap-to-start gate and an end screen with a CTA.
8
-
9
- Reach for it whenever the idea is genuinely three-dimensional — a cube you carve,
10
- a stack you topple, a solid you inspect from all sides. Every other recipe is 2D,
11
- and a 3D idea faked with isometric sprites is not the same game.
12
-
13
- The gameplay is a placeholder — `hero-box` spinning on a `core:Rotate`, plus an
14
- auto-win timer. Replace both; keep the camera, the lights and the gate.
15
-
16
- **The bookkeeping is already built.** `GameRules` keeps score, lives and the clock and
17
- decides win/lose; `ScoreHud` displays them. Your first increment is the *mechanic*: call
18
- `addScore(1)` / `loseLife()` / `finish(true)`, or emit `score-added` / `life-lost` on
19
- `hud-root`. Never write a second score counter, HUD label or result screen.
20
-
21
- **Lighting is load-bearing.** Lit materials with no light render pure black, and
22
- the editor viewport hides that by adding fallback lights the running game has not.
23
- `key-light` and `ambient-light` are why this scene is visible — never delete them.
24
- A mesh that comes out black is reported by the scene lint (`sceneIssues` in
25
- `scene_tree` / `game_observe`).
26
-
27
- **Mobile is the default target.** Meshes use `material.type: lambert`
28
- (diffuse-only), not PBR: the shapes read the same at a fraction of the per-pixel
29
- cost, and new geometry inherits it. `standard` (PBR) is for a `desktop`
30
- `targetPlatform` — switch one mesh with `set_property <node> materialType
31
- standard` when the user asks for a high-end look. `basic` (unlit) is cheaper
32
- still and the one family that cannot render black for want of a light.
33
-
34
- ## Node map
35
-
36
- | id | what |
37
- | --- | --- |
38
- | `game-root` | 3D scene root (`Node3D`) |
39
- | `main-camera` | `Camera3D`, perspective, looking down at the stage from `[0, 3, 8]` |
40
- | `key-light` | `DirectionalLightNode` — the shading light; angle it, don't remove it |
41
- | `ambient-light` | `AmbientLightNode` — the fill that keeps unlit faces readable |
42
- | `ground` | `GeometryMesh` box, 12×0.2×12, the floor slab |
43
- | `hero-box` | `GeometryMesh` box placeholder, spinning on `core:Rotate` |
44
- | `hud-root` | 2D UI layer (`Group2D`, stretched); hosts `GameFlow` (intro → playing → ended), `GameRules` (score/lives/win-lose) and `ScoreHud` (display only) |
45
- | `hud-label` | in-game text line |
46
- | `score-label` | `SCORE n`, driven by the `score-changed` signal |
47
- | `time-label` | seconds — counts up with no time limit, down with one |
48
- | `lives-bar` | `Bar2D` fed by `lives-changed` |
49
- | `intro-overlay` → `intro-dim`, `intro-label` | the tap gate (`scenes/ui/intro.pix3scene`, instanced into `hud-root`); the first tap hides it, starts the game, unlocks browser audio |
50
- | `end-screen` → `end-dim`, `end-label`, `cta-button` | end screen (`scenes/ui/end-screen.pix3scene`, instanced into `hud-root`), `initiallyVisible: false` |
51
- | `cta-button` | hosts `CtaButton` — `playable.gameEnd()` + a `[CtaButton] CTA clicked` log; the store call belongs to the ad network SDK, not to the template |
52
-
53
- End a run with `GameRules.finish(won)` — it writes the outcome text and dispatches
54
- `GameFlow`'s `finish`. One end screen, one phase machine, one debug snapshot (the
55
- rules' numbers ride inside GameFlow's).
56
-
57
- ## Placeholders
58
-
59
- | role | file | node |
60
- | --- | --- | --- |
61
- | hero | (none — `hero-box` is untextured geometry) | `hero-box` |
62
-
63
- ## Tunables
64
-
65
- ```yaml
66
- tunables:
67
- autoWinAfterSec: { node: hud-root, component: "user:GameFlow", property: autoWinAfterSec, min: 0, max: 120, default: 15 }
68
- introNode: { node: hud-root, component: "user:GameFlow", property: introNode, default: intro-overlay }
69
- endNode: { node: hud-root, component: "user:GameFlow", property: endNode, default: end-screen }
70
- skipIntro: { node: hud-root, component: "user:GameFlow", property: skipIntro, default: false }
71
- winMode: { node: hud-root, component: "user:GameRules", property: winMode, default: score }
72
- targetScore: { node: hud-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
73
- timeLimitSec: { node: hud-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
74
- startingLives: { node: hud-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
75
- keyLightIntensity: { node: key-light, property: intensity, min: 0, max: 4, default: 1.2 }
76
- ambientIntensity: { node: ambient-light, property: intensity, min: 0, max: 2, default: 0.5 }
77
- ```
78
-
79
- `component` present → `set_component_property`; absent → `set_property`.
80
-
81
- `skipIntro: true` starts already playing, gate hidden — a prototyping vent, off before
82
- shipping: that first tap is what unlocks browser audio. Mesh colour is not a declared
83
- tunable — it lives under `material.color` and is edited with
84
- `set_property <node> color "#rrggbb"`.
85
-
86
- ## Extension points
87
-
88
- - **Real geometry.** `create_node` more `GeometryMesh` nodes (`box`, `sphere`,
89
- `cylinder`, …) with `position3`, or point `hero-box` at a `.glb` as a
90
- `MeshInstance3D`. A grid or stack of solids is a loop over `create_node`.
91
- - **Picking.** 3D taps come from the raycast API, not 2D hit-testing: read the
92
- pointer in a script and ray-test the meshes. That is the backbone of every
93
- "tap the block" mechanic.
94
- - **Camera moves.** Orbit or pull back by animating `main-camera`'s transform, or
95
- add a `VirtualCamera3D` rig if the game needs blends between framings.
96
- - **Real gameplay.** Set `autoWinAfterSec: 0` and let `GameRules` end the run:
97
- `addScore`/`score-added`, `loseLife`/`life-lost`, and a win condition picked with
98
- `winMode` + `targetScore` + `timeLimitSec` instead of one you write.
99
-
100
- ## Do not touch
101
-
102
- - **`key-light` / `ambient-light`.** Deleting or disabling them makes every lit
103
- mesh black. Re-colour and re-angle freely; keep at least one enabled light.
104
- - **`main-camera`.** A 3D scene with no enabled `Camera3D` draws nothing at all.
105
- - **The `hud-root` layout.** Stretched to the safe area on purpose; nest new UI
106
- inside it, never add a second full-screen root.
107
- - **`GameFlow`'s `introNode` / `endNode` ids.** They address nodes by id: renaming a
108
- node without updating them breaks the phase machine silently. Same for `ScoreHud`'s
109
- `sourceNode` / label ids.
110
- - **The overlay instances.** `intro-overlay` / `end-screen` in `main.pix3scene` are
111
- `instance:` nodes marked `visible: false` — an editor-only hide so the scene opens
112
- on the stage. Keep it; `initiallyVisible` lives in the overlay file (`true` for
113
- the gate, `false` for the end screen), never as an instance override.
114
- - **A second `registerGameDebug`.** `GameFlow` publishes the only one (rules' numbers
115
- merged in); a second silently replaces it. Add fields to that snapshot.
116
-
117
- ## Verify
118
-
119
- - `play_start`, then `game_observe` — check `sceneIssues` is **absent**. A
120
- `lit-material-no-light` or `no-camera-3d` entry means the scene cannot draw.
121
- - Tap the gate: `game_input {type:'tap', target:'intro-overlay'}` and confirm
122
- `intro-overlay` goes invisible and the game phase advances.
123
- - Read scoring back as data: the snapshot carries `score`, `lives`, `timeLeftSec`
124
- and `outcome` next to `phase` — never judge a counter from a picture.
125
- - Judge the win with `game_run` and an `until` that names what success is (e.g.
126
- `end-screen` visible), not with a screenshot — the editor viewport adds
127
- fallback lights, so a picture cannot prove the running game is lit.
1
+ # Recipe: recipe-scene-3d
2
+
3
+ ## What this is
4
+
5
+ The only **3D** recipe: a portrait scene with a real 3D stage — perspective
6
+ `Camera3D`, a key light and an ambient fill, a ground slab and one hero solid —
7
+ under a 2D UI layer with a tap-to-start gate and an end screen with a CTA.
8
+
9
+ Reach for it whenever the idea is genuinely three-dimensional — a cube you carve,
10
+ a stack you topple, a solid you inspect from all sides. Every other recipe is 2D,
11
+ and a 3D idea faked with isometric sprites is not the same game.
12
+
13
+ The gameplay is a placeholder — `hero-box` spinning on a `core:Rotate`, plus an
14
+ auto-win timer. Replace both; keep the camera, the lights and the gate.
15
+
16
+ **The bookkeeping is already built.** `GameRules` keeps score, lives and the clock and
17
+ decides win/lose; `ScoreHud` displays them. Your first increment is the *mechanic*: call
18
+ `addScore(1)` / `loseLife()` / `finish(true)`, or emit `score-added` / `life-lost` on
19
+ `hud-root`. Never write a second score counter, HUD label or result screen.
20
+
21
+ **Lighting is load-bearing.** Lit materials with no light render pure black, and
22
+ the editor viewport hides that by adding fallback lights the running game has not.
23
+ `key-light` and `ambient-light` are why this scene is visible — never delete them.
24
+ A mesh that comes out black is reported by the scene lint (`sceneIssues` in
25
+ `scene_tree` / `game_observe`).
26
+
27
+ **Mobile is the default target.** Meshes use `material.type: lambert`
28
+ (diffuse-only), not PBR: the shapes read the same at a fraction of the per-pixel
29
+ cost, and new geometry inherits it. `standard` (PBR) is for a `desktop`
30
+ `targetPlatform` — switch one mesh with `set_property <node> materialType
31
+ standard` when the user asks for a high-end look. `basic` (unlit) is cheaper
32
+ still and the one family that cannot render black for want of a light.
33
+
34
+ ## Node map
35
+
36
+ | id | what |
37
+ | --- | --- |
38
+ | `game-root` | 3D scene root (`Node3D`) |
39
+ | `main-camera` | `Camera3D`, perspective, looking down at the stage from `[0, 3, 8]` |
40
+ | `key-light` | `DirectionalLightNode` — the shading light; angle it, don't remove it |
41
+ | `ambient-light` | `AmbientLightNode` — the fill that keeps unlit faces readable |
42
+ | `ground` | `GeometryMesh` box, 12×0.2×12, the floor slab |
43
+ | `hero-box` | `GeometryMesh` box placeholder, spinning on `core:Rotate` |
44
+ | `hud-root` | 2D UI layer (`Group2D`, stretched); hosts `GameFlow` (intro → playing → ended), `GameRules` (score/lives/win-lose) and `ScoreHud` (display only) |
45
+ | `hud-label` | in-game text line |
46
+ | `score-label` | `SCORE n`, driven by the `score-changed` signal |
47
+ | `time-label` | seconds — counts up with no time limit, down with one |
48
+ | `lives-bar` | `Bar2D` fed by `lives-changed` |
49
+ | `intro-overlay` → `intro-dim`, `intro-label` | the tap gate (`scenes/ui/intro.pix3scene`, instanced into `hud-root`); the first tap hides it, starts the game, unlocks browser audio |
50
+ | `end-screen` → `end-dim`, `end-label`, `cta-button` | end screen (`scenes/ui/end-screen.pix3scene`, instanced into `hud-root`), `initiallyVisible: false` |
51
+ | `cta-button` | hosts `CtaButton` — `playable.gameEnd()` + a `[CtaButton] CTA clicked` log; the store call belongs to the ad network SDK, not to the template |
52
+
53
+ End a run with `GameRules.finish(won)` — it writes the outcome text and dispatches
54
+ `GameFlow`'s `finish`. One end screen, one phase machine, one debug snapshot (the
55
+ rules' numbers ride inside GameFlow's).
56
+
57
+ ## Placeholders
58
+
59
+ | role | file | node |
60
+ | --- | --- | --- |
61
+ | hero | (none — `hero-box` is untextured geometry) | `hero-box` |
62
+
63
+ ## Tunables
64
+
65
+ ```yaml
66
+ tunables:
67
+ autoWinAfterSec: { node: hud-root, component: "user:GameFlow", property: autoWinAfterSec, min: 0, max: 120, default: 15 }
68
+ introNode: { node: hud-root, component: "user:GameFlow", property: introNode, default: intro-overlay }
69
+ endNode: { node: hud-root, component: "user:GameFlow", property: endNode, default: end-screen }
70
+ skipIntro: { node: hud-root, component: "user:GameFlow", property: skipIntro, default: false }
71
+ winMode: { node: hud-root, component: "user:GameRules", property: winMode, default: score }
72
+ targetScore: { node: hud-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
73
+ timeLimitSec: { node: hud-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
74
+ startingLives: { node: hud-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
75
+ keyLightIntensity: { node: key-light, property: intensity, min: 0, max: 4, default: 1.2 }
76
+ ambientIntensity: { node: ambient-light, property: intensity, min: 0, max: 2, default: 0.5 }
77
+ ```
78
+
79
+ `component` present → `set_component_property`; absent → `set_property`.
80
+
81
+ `skipIntro: true` starts already playing, gate hidden — a prototyping vent, off before
82
+ shipping: that first tap is what unlocks browser audio. Mesh colour is not a declared
83
+ tunable — it lives under `material.color` and is edited with
84
+ `set_property <node> color "#rrggbb"`.
85
+
86
+ ## Extension points
87
+
88
+ - **Real geometry.** `create_node` more `GeometryMesh` nodes (`box`, `sphere`,
89
+ `cylinder`, …) with `position3`, or point `hero-box` at a `.glb` as a
90
+ `MeshInstance3D`. A grid or stack of solids is a loop over `create_node`.
91
+ - **Picking.** 3D taps come from the raycast API, not 2D hit-testing: read the
92
+ pointer in a script and ray-test the meshes. That is the backbone of every
93
+ "tap the block" mechanic.
94
+ - **Camera moves.** Orbit or pull back by animating `main-camera`'s transform, or
95
+ add a `VirtualCamera3D` rig if the game needs blends between framings.
96
+ - **Real gameplay.** Set `autoWinAfterSec: 0` and let `GameRules` end the run:
97
+ `addScore`/`score-added`, `loseLife`/`life-lost`, and a win condition picked with
98
+ `winMode` + `targetScore` + `timeLimitSec` instead of one you write.
99
+
100
+ ## Do not touch
101
+
102
+ - **`key-light` / `ambient-light`.** Deleting or disabling them makes every lit
103
+ mesh black. Re-colour and re-angle freely; keep at least one enabled light.
104
+ - **`main-camera`.** A 3D scene with no enabled `Camera3D` draws nothing at all.
105
+ - **The `hud-root` layout.** Stretched to the safe area on purpose; nest new UI
106
+ inside it, never add a second full-screen root.
107
+ - **`GameFlow`'s `introNode` / `endNode` ids.** They address nodes by id: renaming a
108
+ node without updating them breaks the phase machine silently. Same for `ScoreHud`'s
109
+ `sourceNode` / label ids.
110
+ - **The overlay instances.** `intro-overlay` / `end-screen` in `main.pix3scene` are
111
+ `instance:` nodes marked `visible: false` — an editor-only hide so the scene opens
112
+ on the stage. Keep it; `initiallyVisible` lives in the overlay file (`true` for
113
+ the gate, `false` for the end screen), never as an instance override.
114
+ - **A second `registerGameDebug`.** `GameFlow` publishes the only one (rules' numbers
115
+ merged in); a second silently replaces it. Add fields to that snapshot.
116
+
117
+ ## Verify
118
+
119
+ - `play_start`, then `game_observe` — check `sceneIssues` is **absent**. A
120
+ `lit-material-no-light` or `no-camera-3d` entry means the scene cannot draw.
121
+ - Tap the gate: `game_input {type:'tap', target:'intro-overlay'}` and confirm
122
+ `intro-overlay` goes invisible and the game phase advances.
123
+ - Read scoring back as data: the snapshot carries `score`, `lives`, `timeLeftSec`
124
+ and `outcome` next to `phase` — never judge a counter from a picture.
125
+ - Judge the win with `game_run` and an `until` that names what success is (e.g.
126
+ `end-screen` visible), not with a screenshot — the editor viewport adds
127
+ fallback lights, so a picture cannot prove the running game is lit.
@@ -1,4 +1,4 @@
1
- {
2
- "version": 1,
3
- "proven": []
4
- }
1
+ {
2
+ "version": 1,
3
+ "proven": []
4
+ }
@@ -1,32 +1,32 @@
1
- {
2
- "name": "intro-to-cta",
3
- "description": "Tap through the intro gate, end the run by intent, and press the CTA on the end screen.",
4
- "note": "Run it with game_run {routine:'intro-to-cta'}. Steps are the game_input vocabulary (tap/key/keys/drag/hover/wait/invoke) plus {type:'command', name} for an intent the scene registered; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step. A step's `why` and this `note` are documentation — the runner ignores both — but they are why the routine also reads as a checklist. Note what is NOT here: no step calls a component method directly. Ending the run goes through the `finish` command, because a routine that called the handler would prove the handler works while skipping every wire between the player and it. GameFlow rides the 2D overlay layer (hud-root), above the 3D game-root.",
5
- "scope": "scenes/main.pix3scene",
6
- "params": {},
7
- "uses": ["game-root", "hud-root", "intro-overlay", "end-screen", "cta-button", "hero-box"],
8
- "steps": [
9
- {
10
- "type": "tap",
11
- "target": "intro-overlay",
12
- "why": "The tap gate is the gesture under test: it hides the overlay and unlocks browser audio, so it is driven as a real tap rather than by dispatching start-game."
13
- },
14
- {
15
- "type": "command",
16
- "name": "finish",
17
- "why": "Reveal the end screen by intent instead of waiting out autoWinAfterSec — the timer is a placeholder every real playable replaces, and 15 s of wall clock in every run is a budget nobody gets back."
18
- },
19
- {
20
- "type": "tap",
21
- "target": "cta-button",
22
- "holdMs": 700,
23
- "why": "Button2D needs a real press; this proves the button is on screen and hittable, not just present in the tree."
24
- }
25
- ],
26
- "expect": [
27
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "ended" },
28
- { "kind": "gameState", "path": "introVisible", "op": "eq", "value": false },
29
- { "kind": "gameState", "path": "endVisible", "op": "eq", "value": true },
30
- { "kind": "gameState", "path": "gameEnded", "op": "eq", "value": true }
31
- ]
32
- }
1
+ {
2
+ "name": "intro-to-cta",
3
+ "description": "Tap through the intro gate, end the run by intent, and press the CTA on the end screen.",
4
+ "note": "Run it with game_run {routine:'intro-to-cta'}. Steps are the game_input vocabulary (tap/key/keys/drag/hover/wait/invoke) plus {type:'command', name} for an intent the scene registered; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step. A step's `why` and this `note` are documentation — the runner ignores both — but they are why the routine also reads as a checklist. Note what is NOT here: no step calls a component method directly. Ending the run goes through the `finish` command, because a routine that called the handler would prove the handler works while skipping every wire between the player and it. GameFlow rides the 2D overlay layer (hud-root), above the 3D game-root.",
5
+ "scope": "scenes/main.pix3scene",
6
+ "params": {},
7
+ "uses": ["game-root", "hud-root", "intro-overlay", "end-screen", "cta-button", "hero-box"],
8
+ "steps": [
9
+ {
10
+ "type": "tap",
11
+ "target": "intro-overlay",
12
+ "why": "The tap gate is the gesture under test: it hides the overlay and unlocks browser audio, so it is driven as a real tap rather than by dispatching start-game."
13
+ },
14
+ {
15
+ "type": "command",
16
+ "name": "finish",
17
+ "why": "Reveal the end screen by intent instead of waiting out autoWinAfterSec — the timer is a placeholder every real playable replaces, and 15 s of wall clock in every run is a budget nobody gets back."
18
+ },
19
+ {
20
+ "type": "tap",
21
+ "target": "cta-button",
22
+ "holdMs": 700,
23
+ "why": "Button2D needs a real press; this proves the button is on screen and hittable, not just present in the tree."
24
+ }
25
+ ],
26
+ "expect": [
27
+ { "kind": "gameState", "path": "phase", "op": "eq", "value": "ended" },
28
+ { "kind": "gameState", "path": "introVisible", "op": "eq", "value": false },
29
+ { "kind": "gameState", "path": "endVisible", "op": "eq", "value": true },
30
+ { "kind": "gameState", "path": "gameEnded", "op": "eq", "value": true }
31
+ ]
32
+ }