@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,22 +1,22 @@
1
- # Grid 3D
2
-
3
- A playable skeleton for tap-to-carve 3D puzzles: a solid block of cubes, a
4
- perspective camera looking at it, and a tap that removes one cube. Some cubes
5
- are core — hitting one costs a life. Clear every non-core cube to win.
6
-
7
- It plays as shipped — press play in `scenes/main.pix3scene` before changing
8
- anything, so you know what "working" looked like.
9
-
10
- - `scenes/menu.pix3scene` — entry scene; PLAY transitions to the board.
11
- - `scenes/main.pix3scene` — the board: camera, lights, `board-anchor`, HUD.
12
- - `scenes/ui/result.pix3scene` — the end screen (RETRY / MENU), instanced into
13
- main hidden (`visible: false` is editor-only; `GridRules` shows it at run time).
14
- - `scripts/GridBoard.ts` — builds the cubes and turns a tap into a removal.
15
- - `scripts/GridRules.ts` — score, lives, win/lose, the debug provider.
16
- - `scripts/ScoreHud.ts`, `scripts/MenuFlow.ts` — display and menu, shared with
17
- the other recipes.
18
-
19
- The cubes are generated, not authored: board size lives in `GridBoard`'s config
20
- (`sizeX/sizeY/sizeZ`), so a bigger block is one number, not a scene edit.
21
-
22
- Read `design/recipe.md` for the node map, the tunables and what not to touch.
1
+ # Grid 3D
2
+
3
+ A playable skeleton for tap-to-carve 3D puzzles: a solid block of cubes, a
4
+ perspective camera looking at it, and a tap that removes one cube. Some cubes
5
+ are core — hitting one costs a life. Clear every non-core cube to win.
6
+
7
+ It plays as shipped — press play in `scenes/main.pix3scene` before changing
8
+ anything, so you know what "working" looked like.
9
+
10
+ - `scenes/menu.pix3scene` — entry scene; PLAY transitions to the board.
11
+ - `scenes/main.pix3scene` — the board: camera, lights, `board-anchor`, HUD.
12
+ - `scenes/ui/result.pix3scene` — the end screen (RETRY / MENU), instanced into
13
+ main hidden (`visible: false` is editor-only; `GridRules` shows it at run time).
14
+ - `scripts/GridBoard.ts` — builds the cubes and turns a tap into a removal.
15
+ - `scripts/GridRules.ts` — score, lives, win/lose, the debug provider.
16
+ - `scripts/ScoreHud.ts`, `scripts/MenuFlow.ts` — display and menu, shared with
17
+ the other recipes.
18
+
19
+ The cubes are generated, not authored: board size lives in `GridBoard`'s config
20
+ (`sizeX/sizeY/sizeZ`), so a bigger block is one number, not a scene edit.
21
+
22
+ Read `design/recipe.md` for the node map, the tunables and what not to touch.
@@ -1,110 +1,110 @@
1
- # Recipe: recipe-grid-3d
2
-
3
- ## What this is
4
-
5
- The 3D carving shape: a solid block of cubes, a perspective camera looking at
6
- it, and a tap that removes one cube. Some cubes are **core** — hitting one costs
7
- a life. Clear every non-core cube and the run is won.
8
-
9
- Voxel carving, 3D minesweeper, layer puzzles, tap-to-mine, "chip away to reveal
10
- the shape". If the idea is a 3D thing you touch, start here; if it is a flat
11
- board of tiles, `recipe-tapper-2d` is the cheaper shape.
12
-
13
- **The cubes are generated, not authored.** `GridBoard` builds them under
14
- `board-anchor` on start, so the board size is a config number
15
- (`sizeX`/`sizeY`/`sizeZ`), not a scene edit. Do not hand-place cubes in the
16
- scene — they would not be in the board's map and taps would ignore them.
17
-
18
- **Picking is a 3D raycast.** `GridBoard` converts the pointer to normalized
19
- device coordinates and calls `scene.raycastViewport(nx, ny)`. There are no
20
- `Hitbox2D` groups on a mesh, so the 2D collision helpers do nothing here.
21
-
22
- **Lighting is load-bearing.** `key-light` + `fill-light` are why the block is
23
- visible; lit materials with no light render pure black, and the editor viewport
24
- hides that by adding fallback lights the running game does not have. If
25
- something comes out black, read `sceneIssues` in `scene_tree` / `game_observe`.
26
-
27
- **Mobile is the default target.** Every cube is `material.type: lambert` — a
28
- 4×4×4 board is 64 meshes, and PBR at that count is a phone's whole frame budget.
29
- Past ~8³ move to an `InstancedMesh3D` before you reach for prettier materials.
30
-
31
- ## Node map
32
-
33
- | id | what |
34
- | --- | --- |
35
- | `game-root` | 3D scene root; hosts **both** `GridBoard` and `GridRules` |
36
- | `main-camera` | `Camera3D`, perspective, framed on the block from `[6, 6, 8]` |
37
- | `key-light` | `DirectionalLightNode` — the shading light |
38
- | `fill-light` | `HemisphereLightNode` — keeps unlit faces readable |
39
- | `board-anchor` | empty `Node3D` the generated cubes are parented to; the board centres itself on it |
40
- | `hud-root` | 2D UI layer; hosts `ScoreHud` |
41
- | `score-label` | cleared-cube counter |
42
- | `lives-bar` | `Bar2D` driven by `lives-changed` |
43
- | `result-overlay` → `result-dim`, `result-label`, `retry-button`, `menu-button` | end screen; own file `scenes/ui/result.pix3scene`, instanced into main with `visible: false` (editor-only hide — `GridRules` shows it) |
44
-
45
- Signals, all emitted on `game-root` by `GridBoard` and consumed by `GridRules`:
46
- `board-built` (clearable, cores) · `cell-cleared` (remaining) · `core-hit`
47
- (remaining) · `board-cleared`.
48
-
49
- ## Placeholders
50
-
51
- | role | file | node |
52
- | --- | --- | --- |
53
- | cube | (none — generated `GeometryMesh`, coloured by `cellColor`) | `board-anchor` children |
54
-
55
- ## Tunables
56
-
57
- ```yaml
58
- tunables:
59
- sizeX: { node: game-root, component: "user:GridBoard", property: sizeX, min: 1, max: 10, default: 4 }
60
- sizeY: { node: game-root, component: "user:GridBoard", property: sizeY, min: 1, max: 10, default: 4 }
61
- sizeZ: { node: game-root, component: "user:GridBoard", property: sizeZ, min: 1, max: 10, default: 4 }
62
- coreCount: { node: game-root, component: "user:GridBoard", property: coreCount, min: 0, max: 40, default: 6 }
63
- cellSize: { node: game-root, component: "user:GridBoard", property: cellSize, min: 0.1, max: 3, default: 0.9 }
64
- spacing: { node: game-root, component: "user:GridBoard", property: spacing, min: 0.1, max: 4, default: 1 }
65
- cellColor: { node: game-root, component: "user:GridBoard", property: cellColor, default: "#3ee6c1" }
66
- coreColor: { node: game-root, component: "user:GridBoard", property: coreColor, default: "#ff6b6b" }
67
- revealCores: { node: game-root, component: "user:GridBoard", property: revealCores, default: false }
68
- startingLives: { node: game-root, component: "user:GridRules", property: startingLives, min: 1, max: 9, default: 3 }
69
- timeLimitSec: { node: game-root, component: "user:GridRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
70
- ```
71
-
72
- `component` present → `set_component_property`; absent → `set_property`.
73
-
74
- ## Extension points
75
-
76
- - **A shape inside the block.** `pickCoreKeys` chooses cores at random from the
77
- interior; replace it with a lookup into your own voxel shape and the game
78
- becomes "carve until the statue appears".
79
- - **Layer rules.** `GridBoard.removeCell` is the one place a removal happens —
80
- add a cascade (remove neighbours), a combo counter, or a rule that only
81
- exposed cubes may be tapped, all from there.
82
- - **Camera moves.** Animate `main-camera`'s transform, or add a
83
- `VirtualCamera3D` rig, to let the player orbit the block between taps.
84
- - **Feel.** `clearSound` / `coreSound` take a `res://` audio path; the punch and
85
- the red flash are already wired through `scene.juice`.
86
-
87
- ## Do not touch
88
-
89
- - **`key-light` / `fill-light`.** Remove them and every cube renders black.
90
- - **`main-camera`.** No enabled `Camera3D` means nothing 3D is drawn at all, and
91
- the raycast has no camera to shoot through — taps stop working too.
92
- - **`board-anchor`'s id.** `GridBoard.boardAnchor` addresses it by name; renaming
93
- one without the other leaves the board unbuilt (it logs a warning and stops).
94
- - **The `game-root` id.** Portable tests and routines address the run through it.
95
-
96
- ## Verify
97
-
98
- - `play_start`, then `game_observe` — `sceneIssues` must be **absent**, and
99
- `game.snapshot()` carries `remaining` (cubes still to clear).
100
- - Tap the block: `game_input {type:'tap', x:0, y:0}` and assert `remaining` went
101
- **down** in `game.changed`. That is the proof a tap reached the board — not a
102
- screenshot, which the editor's fallback lighting can flatter. Note the units:
103
- tap coordinates are 2D world space, which is **centred on the origin**, so the
104
- middle of the screen is `(0, 0)` and not `(540, 960)`.
105
- - Win: set `sizeX/sizeY/sizeZ: 1` and `coreCount: 0`, then one tap clears the
106
- board. Read the *transition*, not the end state: the win overlay appears on the
107
- pointer-DOWN that clears the last cube, so the matching pointer-UP can land on
108
- the RETRY button underneath the finger and start a fresh run before you look.
109
- Assert on `board-cleared` / `game-won`, or tap a last cube that is not under
110
- the end screen's buttons.
1
+ # Recipe: recipe-grid-3d
2
+
3
+ ## What this is
4
+
5
+ The 3D carving shape: a solid block of cubes, a perspective camera looking at
6
+ it, and a tap that removes one cube. Some cubes are **core** — hitting one costs
7
+ a life. Clear every non-core cube and the run is won.
8
+
9
+ Voxel carving, 3D minesweeper, layer puzzles, tap-to-mine, "chip away to reveal
10
+ the shape". If the idea is a 3D thing you touch, start here; if it is a flat
11
+ board of tiles, `recipe-tapper-2d` is the cheaper shape.
12
+
13
+ **The cubes are generated, not authored.** `GridBoard` builds them under
14
+ `board-anchor` on start, so the board size is a config number
15
+ (`sizeX`/`sizeY`/`sizeZ`), not a scene edit. Do not hand-place cubes in the
16
+ scene — they would not be in the board's map and taps would ignore them.
17
+
18
+ **Picking is a 3D raycast.** `GridBoard` converts the pointer to normalized
19
+ device coordinates and calls `scene.raycastViewport(nx, ny)`. There are no
20
+ `Hitbox2D` groups on a mesh, so the 2D collision helpers do nothing here.
21
+
22
+ **Lighting is load-bearing.** `key-light` + `fill-light` are why the block is
23
+ visible; lit materials with no light render pure black, and the editor viewport
24
+ hides that by adding fallback lights the running game does not have. If
25
+ something comes out black, read `sceneIssues` in `scene_tree` / `game_observe`.
26
+
27
+ **Mobile is the default target.** Every cube is `material.type: lambert` — a
28
+ 4×4×4 board is 64 meshes, and PBR at that count is a phone's whole frame budget.
29
+ Past ~8³ move to an `InstancedMesh3D` before you reach for prettier materials.
30
+
31
+ ## Node map
32
+
33
+ | id | what |
34
+ | --- | --- |
35
+ | `game-root` | 3D scene root; hosts **both** `GridBoard` and `GridRules` |
36
+ | `main-camera` | `Camera3D`, perspective, framed on the block from `[6, 6, 8]` |
37
+ | `key-light` | `DirectionalLightNode` — the shading light |
38
+ | `fill-light` | `HemisphereLightNode` — keeps unlit faces readable |
39
+ | `board-anchor` | empty `Node3D` the generated cubes are parented to; the board centres itself on it |
40
+ | `hud-root` | 2D UI layer; hosts `ScoreHud` |
41
+ | `score-label` | cleared-cube counter |
42
+ | `lives-bar` | `Bar2D` driven by `lives-changed` |
43
+ | `result-overlay` → `result-dim`, `result-label`, `retry-button`, `menu-button` | end screen; own file `scenes/ui/result.pix3scene`, instanced into main with `visible: false` (editor-only hide — `GridRules` shows it) |
44
+
45
+ Signals, all emitted on `game-root` by `GridBoard` and consumed by `GridRules`:
46
+ `board-built` (clearable, cores) · `cell-cleared` (remaining) · `core-hit`
47
+ (remaining) · `board-cleared`.
48
+
49
+ ## Placeholders
50
+
51
+ | role | file | node |
52
+ | --- | --- | --- |
53
+ | cube | (none — generated `GeometryMesh`, coloured by `cellColor`) | `board-anchor` children |
54
+
55
+ ## Tunables
56
+
57
+ ```yaml
58
+ tunables:
59
+ sizeX: { node: game-root, component: "user:GridBoard", property: sizeX, min: 1, max: 10, default: 4 }
60
+ sizeY: { node: game-root, component: "user:GridBoard", property: sizeY, min: 1, max: 10, default: 4 }
61
+ sizeZ: { node: game-root, component: "user:GridBoard", property: sizeZ, min: 1, max: 10, default: 4 }
62
+ coreCount: { node: game-root, component: "user:GridBoard", property: coreCount, min: 0, max: 40, default: 6 }
63
+ cellSize: { node: game-root, component: "user:GridBoard", property: cellSize, min: 0.1, max: 3, default: 0.9 }
64
+ spacing: { node: game-root, component: "user:GridBoard", property: spacing, min: 0.1, max: 4, default: 1 }
65
+ cellColor: { node: game-root, component: "user:GridBoard", property: cellColor, default: "#3ee6c1" }
66
+ coreColor: { node: game-root, component: "user:GridBoard", property: coreColor, default: "#ff6b6b" }
67
+ revealCores: { node: game-root, component: "user:GridBoard", property: revealCores, default: false }
68
+ startingLives: { node: game-root, component: "user:GridRules", property: startingLives, min: 1, max: 9, default: 3 }
69
+ timeLimitSec: { node: game-root, component: "user:GridRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
70
+ ```
71
+
72
+ `component` present → `set_component_property`; absent → `set_property`.
73
+
74
+ ## Extension points
75
+
76
+ - **A shape inside the block.** `pickCoreKeys` chooses cores at random from the
77
+ interior; replace it with a lookup into your own voxel shape and the game
78
+ becomes "carve until the statue appears".
79
+ - **Layer rules.** `GridBoard.removeCell` is the one place a removal happens —
80
+ add a cascade (remove neighbours), a combo counter, or a rule that only
81
+ exposed cubes may be tapped, all from there.
82
+ - **Camera moves.** Animate `main-camera`'s transform, or add a
83
+ `VirtualCamera3D` rig, to let the player orbit the block between taps.
84
+ - **Feel.** `clearSound` / `coreSound` take a `res://` audio path; the punch and
85
+ the red flash are already wired through `scene.juice`.
86
+
87
+ ## Do not touch
88
+
89
+ - **`key-light` / `fill-light`.** Remove them and every cube renders black.
90
+ - **`main-camera`.** No enabled `Camera3D` means nothing 3D is drawn at all, and
91
+ the raycast has no camera to shoot through — taps stop working too.
92
+ - **`board-anchor`'s id.** `GridBoard.boardAnchor` addresses it by name; renaming
93
+ one without the other leaves the board unbuilt (it logs a warning and stops).
94
+ - **The `game-root` id.** Portable tests and routines address the run through it.
95
+
96
+ ## Verify
97
+
98
+ - `play_start`, then `game_observe` — `sceneIssues` must be **absent**, and
99
+ `game.snapshot()` carries `remaining` (cubes still to clear).
100
+ - Tap the block: `game_input {type:'tap', x:0, y:0}` and assert `remaining` went
101
+ **down** in `game.changed`. That is the proof a tap reached the board — not a
102
+ screenshot, which the editor's fallback lighting can flatter. Note the units:
103
+ tap coordinates are 2D world space, which is **centred on the origin**, so the
104
+ middle of the screen is `(0, 0)` and not `(540, 960)`.
105
+ - Win: set `sizeX/sizeY/sizeZ: 1` and `coreCount: 0`, then one tap clears the
106
+ board. Read the *transition*, not the end state: the win overlay appears on the
107
+ pointer-DOWN that clears the last cube, so the matching pointer-UP can land on
108
+ the RETRY button underneath the finger and start a fresh run before you look.
109
+ Assert on `board-cleared` / `game-won`, or tap a last cube that is not under
110
+ the end screen's buttons.
@@ -1,169 +1,169 @@
1
- # Pix3 Scene File (YAML)
2
- # Grid 3D — the carving board. The cubes are NOT authored here: GridBoard builds
3
- # them under `board-anchor` at start, so the board size is a tunable, not a
4
- # hand-edited tree. Everything else (camera, lights, HUD) is authored here; the
5
- # end screen lives in scenes/ui/result.pix3scene and is instanced in hidden
6
- # (`visible: false` = editor only), so this scene opens on the board.
7
- #
8
- # Stable node ids (design/recipe.md is the contract — do not rename these):
9
- # game-root, main-camera, key-light, fill-light, board-anchor,
10
- # hud-root, score-label, lives-bar,
11
- # result-overlay, result-label, retry-button, menu-button (scenes/ui/result.pix3scene)
12
- version: 1.0.0
13
- metadata:
14
- author: Pix3 Template
15
- description: Tap-to-carve 3D grid with a hidden core
16
- root:
17
- - id: game-root
18
- type: Node3D
19
- name: Game Root
20
- properties:
21
- transform:
22
- position: [0, 0, 0]
23
- rotationEuler: [0, 0, 0]
24
- scale: [1, 1, 1]
25
- components:
26
- - id: grid-board
27
- type: user:GridBoard
28
- enabled: true
29
- config:
30
- boardAnchor: board-anchor
31
- sizeX: 4
32
- sizeY: 4
33
- sizeZ: 4
34
- cellSize: 0.9
35
- spacing: 1
36
- coreCount: 6
37
- cellColor: "#3ee6c1"
38
- coreColor: "#ff6b6b"
39
- revealCores: false
40
- - id: grid-rules
41
- type: user:GridRules
42
- enabled: true
43
- config:
44
- startingLives: 3
45
- timeLimitSec: 0
46
- resultNode: result-overlay
47
- resultLabel: result-label
48
- retryButton: retry-button
49
- menuButton: menu-button
50
- children:
51
- - id: main-camera
52
- type: Camera3D
53
- name: Main Camera
54
- properties:
55
- projection: perspective
56
- fov: 50
57
- near: 0.1
58
- far: 200
59
- # Aimed exactly at the origin, where the board centres itself. Solved for the engine's
60
- # XYZ Euler order (R = Rx·Ry), NOT the yaw-then-pitch YXZ order the numbers would have
61
- # under a camera rig: pitch = -asin(0.6) = -36.87°, yaw = asin(6/|[6,6,8]|) = 30.96°.
62
- # Swap the two and the view tilts off target — invisible on a full board (some cube
63
- # still catches the centre ray) and fatal on a 1x1x1 one, which is why the recipe's
64
- # Verify section shrinks the board.
65
- transform:
66
- position: [6, 6, 8]
67
- rotationEuler: [-36.87, 30.96, 0]
68
- scale: [1, 1, 1]
69
- children: []
70
- - id: key-light
71
- type: DirectionalLightNode
72
- name: Key Light
73
- properties:
74
- intensity: 1.1
75
- color: "#ffffff"
76
- transform:
77
- position: [6, 9, 4]
78
- rotationEuler: [-50, 30, 0]
79
- scale: [1, 1, 1]
80
- children: []
81
- - id: fill-light
82
- type: HemisphereLightNode
83
- name: Fill Light
84
- properties:
85
- skyColor: "#e9f5ff"
86
- groundColor: "#717786"
87
- intensity: 0.7
88
- transform:
89
- position: [0, 0, 0]
90
- rotationEuler: [0, 0, 0]
91
- scale: [1, 1, 1]
92
- children: []
93
- - id: board-anchor
94
- type: Node3D
95
- name: Board Anchor
96
- properties:
97
- transform:
98
- position: [0, 0, 0]
99
- rotationEuler: [0, 0, 0]
100
- scale: [1, 1, 1]
101
- children: []
102
-
103
- - id: hud-root
104
- type: Group2D
105
- name: HUD Root
106
- properties:
107
- width: 1080
108
- height: 1920
109
- layout:
110
- enabled: true
111
- horizontalAlign: stretch
112
- verticalAlign: stretch
113
- components:
114
- - id: score-hud
115
- type: user:ScoreHud
116
- enabled: true
117
- config:
118
- sourceNode: game-root
119
- scoreLabel: score-label
120
- scoreFormat: "CLEARED {value}"
121
- timeLabel: ""
122
- livesBar: lives-bar
123
- children:
124
- - id: score-label
125
- type: Label2D
126
- name: Score Label
127
- properties:
128
- label: "CLEARED 0"
129
- labelFontSize: 48
130
- labelColor: "#f2f2f2"
131
- labelAlign: center
132
- transform:
133
- position: [0, 820]
134
- scale: [1, 1]
135
- rotation: 0
136
- layout:
137
- enabled: true
138
- horizontalAlign: center
139
- verticalAlign: top
140
- children: []
141
- - id: lives-bar
142
- type: Bar2D
143
- name: Lives Bar
144
- properties:
145
- width: 360
146
- height: 26
147
- value: 3
148
- maxValue: 3
149
- barColor: "#ff6b6b"
150
- backBackgroundColor: "#1b1f2a"
151
- transform:
152
- position: [0, 740]
153
- scale: [1, 1]
154
- rotation: 0
155
- layout:
156
- enabled: true
157
- horizontalAlign: center
158
- verticalAlign: top
159
- children: []
160
- # Win/lose screen — authored in scenes/ui/result.pix3scene so this scene
161
- # opens on the BOARD, not on an end card. `visible: false` hides it in the
162
- # EDITOR only; `initiallyVisible` (in the overlay file) is what play mode
163
- # reads, and GridRules reveals it on the win/lose transition. Tick the eye
164
- # to preview it composited over the board.
165
- - id: result-overlay
166
- name: Result Overlay
167
- instance: res://scenes/ui/result.pix3scene
168
- properties:
169
- visible: false
1
+ # Pix3 Scene File (YAML)
2
+ # Grid 3D — the carving board. The cubes are NOT authored here: GridBoard builds
3
+ # them under `board-anchor` at start, so the board size is a tunable, not a
4
+ # hand-edited tree. Everything else (camera, lights, HUD) is authored here; the
5
+ # end screen lives in scenes/ui/result.pix3scene and is instanced in hidden
6
+ # (`visible: false` = editor only), so this scene opens on the board.
7
+ #
8
+ # Stable node ids (design/recipe.md is the contract — do not rename these):
9
+ # game-root, main-camera, key-light, fill-light, board-anchor,
10
+ # hud-root, score-label, lives-bar,
11
+ # result-overlay, result-label, retry-button, menu-button (scenes/ui/result.pix3scene)
12
+ version: 1.0.0
13
+ metadata:
14
+ author: Pix3 Template
15
+ description: Tap-to-carve 3D grid with a hidden core
16
+ root:
17
+ - id: game-root
18
+ type: Node3D
19
+ name: Game Root
20
+ properties:
21
+ transform:
22
+ position: [0, 0, 0]
23
+ rotationEuler: [0, 0, 0]
24
+ scale: [1, 1, 1]
25
+ components:
26
+ - id: grid-board
27
+ type: user:GridBoard
28
+ enabled: true
29
+ config:
30
+ boardAnchor: board-anchor
31
+ sizeX: 4
32
+ sizeY: 4
33
+ sizeZ: 4
34
+ cellSize: 0.9
35
+ spacing: 1
36
+ coreCount: 6
37
+ cellColor: "#3ee6c1"
38
+ coreColor: "#ff6b6b"
39
+ revealCores: false
40
+ - id: grid-rules
41
+ type: user:GridRules
42
+ enabled: true
43
+ config:
44
+ startingLives: 3
45
+ timeLimitSec: 0
46
+ resultNode: result-overlay
47
+ resultLabel: result-label
48
+ retryButton: retry-button
49
+ menuButton: menu-button
50
+ children:
51
+ - id: main-camera
52
+ type: Camera3D
53
+ name: Main Camera
54
+ properties:
55
+ projection: perspective
56
+ fov: 50
57
+ near: 0.1
58
+ far: 200
59
+ # Aimed exactly at the origin, where the board centres itself. Solved for the engine's
60
+ # XYZ Euler order (R = Rx·Ry), NOT the yaw-then-pitch YXZ order the numbers would have
61
+ # under a camera rig: pitch = -asin(0.6) = -36.87°, yaw = asin(6/|[6,6,8]|) = 30.96°.
62
+ # Swap the two and the view tilts off target — invisible on a full board (some cube
63
+ # still catches the centre ray) and fatal on a 1x1x1 one, which is why the recipe's
64
+ # Verify section shrinks the board.
65
+ transform:
66
+ position: [6, 6, 8]
67
+ rotationEuler: [-36.87, 30.96, 0]
68
+ scale: [1, 1, 1]
69
+ children: []
70
+ - id: key-light
71
+ type: DirectionalLightNode
72
+ name: Key Light
73
+ properties:
74
+ intensity: 1.1
75
+ color: "#ffffff"
76
+ transform:
77
+ position: [6, 9, 4]
78
+ rotationEuler: [-50, 30, 0]
79
+ scale: [1, 1, 1]
80
+ children: []
81
+ - id: fill-light
82
+ type: HemisphereLightNode
83
+ name: Fill Light
84
+ properties:
85
+ skyColor: "#e9f5ff"
86
+ groundColor: "#717786"
87
+ intensity: 0.7
88
+ transform:
89
+ position: [0, 0, 0]
90
+ rotationEuler: [0, 0, 0]
91
+ scale: [1, 1, 1]
92
+ children: []
93
+ - id: board-anchor
94
+ type: Node3D
95
+ name: Board Anchor
96
+ properties:
97
+ transform:
98
+ position: [0, 0, 0]
99
+ rotationEuler: [0, 0, 0]
100
+ scale: [1, 1, 1]
101
+ children: []
102
+
103
+ - id: hud-root
104
+ type: Group2D
105
+ name: HUD Root
106
+ properties:
107
+ width: 1080
108
+ height: 1920
109
+ layout:
110
+ enabled: true
111
+ horizontalAlign: stretch
112
+ verticalAlign: stretch
113
+ components:
114
+ - id: score-hud
115
+ type: user:ScoreHud
116
+ enabled: true
117
+ config:
118
+ sourceNode: game-root
119
+ scoreLabel: score-label
120
+ scoreFormat: "CLEARED {value}"
121
+ timeLabel: ""
122
+ livesBar: lives-bar
123
+ children:
124
+ - id: score-label
125
+ type: Label2D
126
+ name: Score Label
127
+ properties:
128
+ label: "CLEARED 0"
129
+ labelFontSize: 48
130
+ labelColor: "#f2f2f2"
131
+ labelAlign: center
132
+ transform:
133
+ position: [0, 820]
134
+ scale: [1, 1]
135
+ rotation: 0
136
+ layout:
137
+ enabled: true
138
+ horizontalAlign: center
139
+ verticalAlign: top
140
+ children: []
141
+ - id: lives-bar
142
+ type: Bar2D
143
+ name: Lives Bar
144
+ properties:
145
+ width: 360
146
+ height: 26
147
+ value: 3
148
+ maxValue: 3
149
+ barColor: "#ff6b6b"
150
+ backBackgroundColor: "#1b1f2a"
151
+ transform:
152
+ position: [0, 740]
153
+ scale: [1, 1]
154
+ rotation: 0
155
+ layout:
156
+ enabled: true
157
+ horizontalAlign: center
158
+ verticalAlign: top
159
+ children: []
160
+ # Win/lose screen — authored in scenes/ui/result.pix3scene so this scene
161
+ # opens on the BOARD, not on an end card. `visible: false` hides it in the
162
+ # EDITOR only; `initiallyVisible` (in the overlay file) is what play mode
163
+ # reads, and GridRules reveals it on the win/lose transition. Tick the eye
164
+ # to preview it composited over the board.
165
+ - id: result-overlay
166
+ name: Result Overlay
167
+ instance: res://scenes/ui/result.pix3scene
168
+ properties:
169
+ visible: false