@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.
- package/LICENSE +201 -201
- package/README.md +788 -754
- package/dist/index.js +208 -203
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-7MZGPAUK.mjs} +1 -1
- package/dist/smoke/prebuilt/chunk-7WOFGLW3.mjs +239 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/bundle-main.js +13 -13
- package/dist/validate/prebuilt/chunk-HKG6YI6Y.js +239 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
- package/kit/files/.claude/skills/pix3-nodes/reference.md +32 -24
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -28
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
- package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
- package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
- package/kit/files/AGENTS.md +203 -203
- package/kit/files/CLAUDE.md +1 -1
- package/kit/kit.json +2 -2
- package/package.json +54 -54
- package/templates/empty-2d/files/README.md +20 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
- package/templates/empty-2d/template.yaml +7 -7
- package/templates/empty-3d/files/README.md +20 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
- package/templates/empty-3d/template.yaml +7 -7
- package/templates/idea-blank/files/README.md +22 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
- package/templates/idea-blank/template.yaml +10 -10
- package/templates/minigame-2d/files/README.md +44 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
- package/templates/minigame-2d/template.yaml +10 -10
- package/templates/playable-2d/files/README.md +30 -30
- package/templates/playable-2d/files/design/recipe.md +83 -83
- package/templates/playable-2d/files/design/tests/reachability.json +4 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
- package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
- package/templates/playable-2d/template.yaml +9 -9
- package/templates/playable-3d/files/README.md +32 -32
- package/templates/playable-3d/files/design/recipe.md +127 -127
- package/templates/playable-3d/files/design/tests/reachability.json +4 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
- package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
- package/templates/playable-3d/template.yaml +9 -9
- package/templates/recipe-arena-2d/files/README.md +48 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
- package/templates/recipe-arena-2d/template.yaml +10 -10
- package/templates/recipe-blank-2d/files/README.md +61 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
- package/templates/recipe-blank-2d/template.yaml +10 -10
- package/templates/recipe-bouncer-2d/files/README.md +81 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
- package/templates/recipe-bouncer-2d/template.yaml +10 -10
- package/templates/recipe-grid-3d/files/README.md +22 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-grid-3d/template.yaml +10 -10
- package/templates/recipe-tapper-2d/files/README.md +39 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
- package/templates/recipe-tapper-2d/template.yaml +10 -10
- package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
- 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
|