@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,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
|
+
}
|