@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,148 +1,148 @@
|
|
|
1
|
-
# Recipe: recipe-blank-2d
|
|
2
|
-
|
|
3
|
-
## What this is
|
|
4
|
-
|
|
5
|
-
**Nothing plays yet, on purpose.** A bounded field, a HUD, and score / lives /
|
|
6
|
-
timer / win-lose bookkeeping that already works — and no mechanic of any kind,
|
|
7
|
-
including controls.
|
|
8
|
-
|
|
9
|
-
Pick this recipe when the idea's core loop is not what another recipe ships:
|
|
10
|
-
grid or turn-based movement (snake, sokoban, match-3), word / card / board games,
|
|
11
|
-
builders, idle games without a ball. **Your first increment is the core
|
|
12
|
-
mechanic itself, controls included** — that is the trade being made here, and it
|
|
13
|
-
beats spending the first increment deleting somebody else's mechanic.
|
|
14
|
-
|
|
15
|
-
Everything *around* the mechanic is done: you never hand-roll a score counter, a
|
|
16
|
-
lives bar, a timer, an end screen or a retry. The look is done too: `post-fx`
|
|
17
|
-
blooms anything bright and `bg-glow` lights the field, so a sprite tinted with a
|
|
18
|
-
palette accent already glows on the first frame.
|
|
19
|
-
|
|
20
|
-
## Node map
|
|
21
|
-
|
|
22
|
-
| id | what |
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| `post-fx` | `PostProcess` (`affect2D`): bloom + vignette — tune, never delete |
|
|
25
|
-
| `game-root` | root; hosts `GameRules`; every HUD signal is emitted here |
|
|
26
|
-
| `game-background` | full-screen `ColorRect2D` (palette background) |
|
|
27
|
-
| `bg-glow` | full-screen `Sprite2D`, the tinted radial gradient over it |
|
|
28
|
-
| `board` | the play field (`Group2D`) — **build your mechanic inside this** |
|
|
29
|
-
| `board-floor` | the field's visible plate; its rect is the field's extent |
|
|
30
|
-
| `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
|
|
31
|
-
| `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
|
|
32
|
-
| `result-overlay` / `result-dim` / `result-label` / `retry-button` | end screen, own file `scenes/ui/result.pix3scene`; instanced into main with `visible: false` (editor-only hide — `GameRules` shows it) |
|
|
33
|
-
|
|
34
|
-
Signals on `game-root`: your mechanic emits `score-added` (amount) and
|
|
35
|
-
`life-lost` (amount); `GameRules` answers `score-changed`, `lives-changed`,
|
|
36
|
-
`time-changed`, and `game-won` / `game-lost`.
|
|
37
|
-
|
|
38
|
-
There is **one gameplay scene** (`scenes/main.pix3scene`) and no menu — see
|
|
39
|
-
Extension points if the game eventually needs one.
|
|
40
|
-
|
|
41
|
-
## Placeholders
|
|
42
|
-
|
|
43
|
-
A **shape library**, not entities: near-white PNGs already tinted with the
|
|
44
|
-
palette at T0 (the role column decides the colour). Put them on a `Sprite2D`
|
|
45
|
-
sized to the thing they stand for, and recolour with `effects: [{type: core:tint,
|
|
46
|
-
params: {color}}]` when you need another accent. **A round thing is
|
|
47
|
-
`ph-circle`/`ph-orb`, a coin or bumper is `ph-ring`, a bar or paddle is
|
|
48
|
-
`ph-capsule`, a spark or star is `ph-star`.** A `ColorRect2D` is for panels,
|
|
49
|
-
floors and bars — never for a ball, a coin, an enemy or anything the player
|
|
50
|
-
looks at: a square ball on the first frame is what the user remembers. Real art
|
|
51
|
-
replaces these through `generate_asset` in the art pass.
|
|
52
|
-
|
|
53
|
-
| role | file | node/prefab |
|
|
54
|
-
| --- | --- | --- |
|
|
55
|
-
| background | `sprites/ph-bg.png` | `bg-glow` (radial gradient) |
|
|
56
|
-
| player | `sprites/ph-circle.png` | (library — a solid disc with a soft edge) |
|
|
57
|
-
| avatar | `sprites/ph-orb.png` | (library — a disc with a bright rim) |
|
|
58
|
-
| collectible | `sprites/ph-ring.png` | (library — a ring; bumpers, coins, targets) |
|
|
59
|
-
| hazard | `sprites/ph-star.png` | (library — a four-point spark) |
|
|
60
|
-
| ui | `sprites/ph-capsule.png` | (library — a rounded bar; paddles, platforms, pills) |
|
|
61
|
-
|
|
62
|
-
## Tunables
|
|
63
|
-
|
|
64
|
-
```yaml
|
|
65
|
-
tunables:
|
|
66
|
-
winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
|
|
67
|
-
targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
|
|
68
|
-
timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
|
|
69
|
-
startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
|
|
70
|
-
bgColor: { node: game-background, property: color, default: "#12141c" }
|
|
71
|
-
boardColor: { node: board-floor, property: color, default: "#1d212e" }
|
|
72
|
-
bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.8 }
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
`component` present → `set_component_property`; absent → `set_property`. The
|
|
76
|
-
schema clamps out-of-range values rather than rejecting them.
|
|
77
|
-
|
|
78
|
-
`timeLimitSec: 0` means **no clock**: the run ends on the target score or on
|
|
79
|
-
running out of lives, and `time-label` counts elapsed seconds up. Set a limit and
|
|
80
|
-
it counts down and becomes a deadline (see `winMode`).
|
|
81
|
-
|
|
82
|
-
## Extension points
|
|
83
|
-
|
|
84
|
-
- **The mechanic.** Write a script under `scripts/`, attach it to a node inside
|
|
85
|
-
`board`, and let it own input and movement. It scores through `GameRules` —
|
|
86
|
-
either `node.emit('score-added', 1)` on `game-root`, or a direct
|
|
87
|
-
`addScore(1)` / `loseLife()` / `finish(true)` on the component. Nothing else in
|
|
88
|
-
the project needs to change for the HUD and the end screen to work.
|
|
89
|
-
- **Collision, when the mechanic needs it.** Three tiers, cheapest first. Overlap
|
|
90
|
-
queries with no response: `scene.collision2d` + `core:Hitbox2D` (axis-aligned).
|
|
91
|
-
Bouncing, pushing, gravity, sensors: the engine's own 2D rigid-body solver —
|
|
92
|
-
`core:PhysicsBody2D` + `core:Collider2D` on the same node (a `core:Collider2D`
|
|
93
|
-
alone is static world geometry), `scene.physics2d.getBody(node)` for
|
|
94
|
-
`applyImpulse` / `setVelocity` / `teleport`, `setGravity(0, 0)` for top-down.
|
|
95
|
-
**Never hand-write a 2D solver, and do not import rapier** (3D only, ~2 MB wasm).
|
|
96
|
-
Grid games usually need no collision at all — compare cell coordinates.
|
|
97
|
-
- **Spawning.** `scene.instantiate` a prefab into a container node, `queueFree`
|
|
98
|
-
it when it leaves the field, and add that container to `GameRules.freezeNodes`
|
|
99
|
-
so it — and every instance under it — stops on game over. Give your spawner a `clear()` method and `resetRun()`
|
|
100
|
-
will empty the field for you.
|
|
101
|
-
- **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
|
|
102
|
-
`scene.juice.burst({x, y})`, `floatText('+5', {at: node})`, `punchScale` /
|
|
103
|
-
`shake` / `flash`, `scene.audio.sfx('score')`, `scene.time.hitstop(50)` on a
|
|
104
|
-
contact START; `scene.juice.trail(ball)` for a light streak; `scene.tween.to(node,
|
|
105
|
-
{scale: 1.2}, {durationSec: 0.15, yoyo: true, repeat: 1})` / `crossFade(a, b)` for any
|
|
106
|
-
motion or panel switch instead of hand-lerping. Bright sprites bloom through `post-fx`
|
|
107
|
-
by themselves.
|
|
108
|
-
- **A menu, later.** Create `scenes/menu.pix3scene` with a root carrying a script
|
|
109
|
-
that calls `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`
|
|
110
|
-
on PLAY, then set Project Settings → Default Export Scene Path to it. Do this
|
|
111
|
-
**after** the game is fun, never before: while iterating, the menu is a screen
|
|
112
|
-
between you and the thing you are working on.
|
|
113
|
-
|
|
114
|
-
## Ending a run belongs to GameRules
|
|
115
|
-
|
|
116
|
-
The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
|
|
117
|
-
start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
|
|
118
|
-
writes the result text, enables RETRY and freezes the `freezeNodes` nodes. For a
|
|
119
|
-
custom win/lose condition call `finish(true|false)` — do NOT show the overlay
|
|
120
|
-
yourself. A hand-rolled ending leaves RETRY on screen with its handler bound and
|
|
121
|
-
`enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
|
|
122
|
-
`control: { enabled: false }`; that is the tell).
|
|
123
|
-
|
|
124
|
-
## Do not touch
|
|
125
|
-
|
|
126
|
-
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
127
|
-
- `GameRules`' ownership of `result-overlay` and `retry-button`.
|
|
128
|
-
- `post-fx`: keep the id and `affect2D: true` — without it a 2D scene gets no
|
|
129
|
-
bloom. Tune the values, don't delete the node.
|
|
130
|
-
- Do not import rapier, and do not add a menu scene early.
|
|
131
|
-
|
|
132
|
-
## Playtest contract
|
|
133
|
-
|
|
134
|
-
terminalVisual: static
|
|
135
|
-
terminalRestartRoutine: terminal-retry
|
|
136
|
-
|
|
137
|
-
## Verify
|
|
138
|
-
|
|
139
|
-
1. `play_start` on `scenes/main.pix3scene` — `game_observe` reports the snapshot
|
|
140
|
-
with `phase: playing`, `score: 0`, `lives: 3`. That is the baseline, before any
|
|
141
|
-
mechanic exists.
|
|
142
|
-
2. After the first mechanic increment, prove it by **state delta**, not by a
|
|
143
|
-
screenshot: `game_input` the real input with `observe:` on the node that should
|
|
144
|
-
react, and read `moved` / `activity` / the `game.changed` diff.
|
|
145
|
-
3. Scoring is proven the same way: one real input, then `score` up in the snapshot
|
|
146
|
-
(and `score-label` reading `SCORE 1`).
|
|
147
|
-
4. `targetScore: 1`, replay, score once → `result-overlay` visible with `YOU WIN!`;
|
|
148
|
-
`retry-button` (or the `restart` intent) starts a fresh run.
|
|
1
|
+
# Recipe: recipe-blank-2d
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
**Nothing plays yet, on purpose.** A bounded field, a HUD, and score / lives /
|
|
6
|
+
timer / win-lose bookkeeping that already works — and no mechanic of any kind,
|
|
7
|
+
including controls.
|
|
8
|
+
|
|
9
|
+
Pick this recipe when the idea's core loop is not what another recipe ships:
|
|
10
|
+
grid or turn-based movement (snake, sokoban, match-3), word / card / board games,
|
|
11
|
+
builders, idle games without a ball. **Your first increment is the core
|
|
12
|
+
mechanic itself, controls included** — that is the trade being made here, and it
|
|
13
|
+
beats spending the first increment deleting somebody else's mechanic.
|
|
14
|
+
|
|
15
|
+
Everything *around* the mechanic is done: you never hand-roll a score counter, a
|
|
16
|
+
lives bar, a timer, an end screen or a retry. The look is done too: `post-fx`
|
|
17
|
+
blooms anything bright and `bg-glow` lights the field, so a sprite tinted with a
|
|
18
|
+
palette accent already glows on the first frame.
|
|
19
|
+
|
|
20
|
+
## Node map
|
|
21
|
+
|
|
22
|
+
| id | what |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `post-fx` | `PostProcess` (`affect2D`): bloom + vignette — tune, never delete |
|
|
25
|
+
| `game-root` | root; hosts `GameRules`; every HUD signal is emitted here |
|
|
26
|
+
| `game-background` | full-screen `ColorRect2D` (palette background) |
|
|
27
|
+
| `bg-glow` | full-screen `Sprite2D`, the tinted radial gradient over it |
|
|
28
|
+
| `board` | the play field (`Group2D`) — **build your mechanic inside this** |
|
|
29
|
+
| `board-floor` | the field's visible plate; its rect is the field's extent |
|
|
30
|
+
| `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
|
|
31
|
+
| `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
|
|
32
|
+
| `result-overlay` / `result-dim` / `result-label` / `retry-button` | end screen, own file `scenes/ui/result.pix3scene`; instanced into main with `visible: false` (editor-only hide — `GameRules` shows it) |
|
|
33
|
+
|
|
34
|
+
Signals on `game-root`: your mechanic emits `score-added` (amount) and
|
|
35
|
+
`life-lost` (amount); `GameRules` answers `score-changed`, `lives-changed`,
|
|
36
|
+
`time-changed`, and `game-won` / `game-lost`.
|
|
37
|
+
|
|
38
|
+
There is **one gameplay scene** (`scenes/main.pix3scene`) and no menu — see
|
|
39
|
+
Extension points if the game eventually needs one.
|
|
40
|
+
|
|
41
|
+
## Placeholders
|
|
42
|
+
|
|
43
|
+
A **shape library**, not entities: near-white PNGs already tinted with the
|
|
44
|
+
palette at T0 (the role column decides the colour). Put them on a `Sprite2D`
|
|
45
|
+
sized to the thing they stand for, and recolour with `effects: [{type: core:tint,
|
|
46
|
+
params: {color}}]` when you need another accent. **A round thing is
|
|
47
|
+
`ph-circle`/`ph-orb`, a coin or bumper is `ph-ring`, a bar or paddle is
|
|
48
|
+
`ph-capsule`, a spark or star is `ph-star`.** A `ColorRect2D` is for panels,
|
|
49
|
+
floors and bars — never for a ball, a coin, an enemy or anything the player
|
|
50
|
+
looks at: a square ball on the first frame is what the user remembers. Real art
|
|
51
|
+
replaces these through `generate_asset` in the art pass.
|
|
52
|
+
|
|
53
|
+
| role | file | node/prefab |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| background | `sprites/ph-bg.png` | `bg-glow` (radial gradient) |
|
|
56
|
+
| player | `sprites/ph-circle.png` | (library — a solid disc with a soft edge) |
|
|
57
|
+
| avatar | `sprites/ph-orb.png` | (library — a disc with a bright rim) |
|
|
58
|
+
| collectible | `sprites/ph-ring.png` | (library — a ring; bumpers, coins, targets) |
|
|
59
|
+
| hazard | `sprites/ph-star.png` | (library — a four-point spark) |
|
|
60
|
+
| ui | `sprites/ph-capsule.png` | (library — a rounded bar; paddles, platforms, pills) |
|
|
61
|
+
|
|
62
|
+
## Tunables
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
tunables:
|
|
66
|
+
winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
|
|
67
|
+
targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
|
|
68
|
+
timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
|
|
69
|
+
startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
|
|
70
|
+
bgColor: { node: game-background, property: color, default: "#12141c" }
|
|
71
|
+
boardColor: { node: board-floor, property: color, default: "#1d212e" }
|
|
72
|
+
bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.8 }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`component` present → `set_component_property`; absent → `set_property`. The
|
|
76
|
+
schema clamps out-of-range values rather than rejecting them.
|
|
77
|
+
|
|
78
|
+
`timeLimitSec: 0` means **no clock**: the run ends on the target score or on
|
|
79
|
+
running out of lives, and `time-label` counts elapsed seconds up. Set a limit and
|
|
80
|
+
it counts down and becomes a deadline (see `winMode`).
|
|
81
|
+
|
|
82
|
+
## Extension points
|
|
83
|
+
|
|
84
|
+
- **The mechanic.** Write a script under `scripts/`, attach it to a node inside
|
|
85
|
+
`board`, and let it own input and movement. It scores through `GameRules` —
|
|
86
|
+
either `node.emit('score-added', 1)` on `game-root`, or a direct
|
|
87
|
+
`addScore(1)` / `loseLife()` / `finish(true)` on the component. Nothing else in
|
|
88
|
+
the project needs to change for the HUD and the end screen to work.
|
|
89
|
+
- **Collision, when the mechanic needs it.** Three tiers, cheapest first. Overlap
|
|
90
|
+
queries with no response: `scene.collision2d` + `core:Hitbox2D` (axis-aligned).
|
|
91
|
+
Bouncing, pushing, gravity, sensors: the engine's own 2D rigid-body solver —
|
|
92
|
+
`core:PhysicsBody2D` + `core:Collider2D` on the same node (a `core:Collider2D`
|
|
93
|
+
alone is static world geometry), `scene.physics2d.getBody(node)` for
|
|
94
|
+
`applyImpulse` / `setVelocity` / `teleport`, `setGravity(0, 0)` for top-down.
|
|
95
|
+
**Never hand-write a 2D solver, and do not import rapier** (3D only, ~2 MB wasm).
|
|
96
|
+
Grid games usually need no collision at all — compare cell coordinates.
|
|
97
|
+
- **Spawning.** `scene.instantiate` a prefab into a container node, `queueFree`
|
|
98
|
+
it when it leaves the field, and add that container to `GameRules.freezeNodes`
|
|
99
|
+
so it — and every instance under it — stops on game over. Give your spawner a `clear()` method and `resetRun()`
|
|
100
|
+
will empty the field for you.
|
|
101
|
+
- **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
|
|
102
|
+
`scene.juice.burst({x, y})`, `floatText('+5', {at: node})`, `punchScale` /
|
|
103
|
+
`shake` / `flash`, `scene.audio.sfx('score')`, `scene.time.hitstop(50)` on a
|
|
104
|
+
contact START; `scene.juice.trail(ball)` for a light streak; `scene.tween.to(node,
|
|
105
|
+
{scale: 1.2}, {durationSec: 0.15, yoyo: true, repeat: 1})` / `crossFade(a, b)` for any
|
|
106
|
+
motion or panel switch instead of hand-lerping. Bright sprites bloom through `post-fx`
|
|
107
|
+
by themselves.
|
|
108
|
+
- **A menu, later.** Create `scenes/menu.pix3scene` with a root carrying a script
|
|
109
|
+
that calls `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`
|
|
110
|
+
on PLAY, then set Project Settings → Default Export Scene Path to it. Do this
|
|
111
|
+
**after** the game is fun, never before: while iterating, the menu is a screen
|
|
112
|
+
between you and the thing you are working on.
|
|
113
|
+
|
|
114
|
+
## Ending a run belongs to GameRules
|
|
115
|
+
|
|
116
|
+
The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
|
|
117
|
+
start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
|
|
118
|
+
writes the result text, enables RETRY and freezes the `freezeNodes` nodes. For a
|
|
119
|
+
custom win/lose condition call `finish(true|false)` — do NOT show the overlay
|
|
120
|
+
yourself. A hand-rolled ending leaves RETRY on screen with its handler bound and
|
|
121
|
+
`enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
|
|
122
|
+
`control: { enabled: false }`; that is the tell).
|
|
123
|
+
|
|
124
|
+
## Do not touch
|
|
125
|
+
|
|
126
|
+
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
127
|
+
- `GameRules`' ownership of `result-overlay` and `retry-button`.
|
|
128
|
+
- `post-fx`: keep the id and `affect2D: true` — without it a 2D scene gets no
|
|
129
|
+
bloom. Tune the values, don't delete the node.
|
|
130
|
+
- Do not import rapier, and do not add a menu scene early.
|
|
131
|
+
|
|
132
|
+
## Playtest contract
|
|
133
|
+
|
|
134
|
+
terminalVisual: static
|
|
135
|
+
terminalRestartRoutine: terminal-retry
|
|
136
|
+
|
|
137
|
+
## Verify
|
|
138
|
+
|
|
139
|
+
1. `play_start` on `scenes/main.pix3scene` — `game_observe` reports the snapshot
|
|
140
|
+
with `phase: playing`, `score: 0`, `lives: 3`. That is the baseline, before any
|
|
141
|
+
mechanic exists.
|
|
142
|
+
2. After the first mechanic increment, prove it by **state delta**, not by a
|
|
143
|
+
screenshot: `game_input` the real input with `observe:` on the node that should
|
|
144
|
+
react, and read `moved` / `activity` / the `game.changed` diff.
|
|
145
|
+
3. Scoring is proven the same way: one real input, then `score` up in the snapshot
|
|
146
|
+
(and `score-label` reading `SCORE 1`).
|
|
147
|
+
4. `targetScore: 1`, replay, score once → `result-overlay` visible with `YOU WIN!`;
|
|
148
|
+
`retry-button` (or the `restart` intent) starts a fresh run.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"proven": []
|
|
4
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"proven": []
|
|
4
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "restart",
|
|
3
|
-
"description": "Dispatch the run's `restart` intent and confirm a fresh run came back up.",
|
|
4
|
-
"note": "This is a FORMAT SEED, not a real test of your game — there is no mechanic here yet to test. Keep it (restart stays worth proving after a scene reload) and add a routine per mechanic next to it: run one with game_run {routine:'<name>'}. Steps are the game_input vocabulary plus {type:'command'} for a registered intent; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step against the frame pair around the run. Only a STEP takes a `why`. Prefer a `gameState` predicate over a node one wherever GameRules already publishes the value — it survives every relayout of the HUD.",
|
|
5
|
-
"scope": "scenes/main.pix3scene",
|
|
6
|
-
"params": {},
|
|
7
|
-
"uses": ["game-root", "result-overlay", "retry-button"],
|
|
8
|
-
"steps": [
|
|
9
|
-
{
|
|
10
|
-
"type": "command",
|
|
11
|
-
"name": "restart",
|
|
12
|
-
"why": "The intent RETRY dispatches, taken at the highest channel: it proves the wire without depending on where the button sits, and the dispatch is journalled with the frame it happened on."
|
|
13
|
-
}
|
|
14
|
-
],
|
|
15
|
-
"expect": [
|
|
16
|
-
{ "kind": "command", "name": "restart" },
|
|
17
|
-
{ "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" },
|
|
18
|
-
{ "kind": "gameState", "path": "score", "op": "eq", "value": 0 },
|
|
19
|
-
{
|
|
20
|
-
"kind": "nodeProperty",
|
|
21
|
-
"name": "result-overlay",
|
|
22
|
-
"path": "visible",
|
|
23
|
-
"op": "eq",
|
|
24
|
-
"value": false
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "restart",
|
|
3
|
+
"description": "Dispatch the run's `restart` intent and confirm a fresh run came back up.",
|
|
4
|
+
"note": "This is a FORMAT SEED, not a real test of your game — there is no mechanic here yet to test. Keep it (restart stays worth proving after a scene reload) and add a routine per mechanic next to it: run one with game_run {routine:'<name>'}. Steps are the game_input vocabulary plus {type:'command'} for a registered intent; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step against the frame pair around the run. Only a STEP takes a `why`. Prefer a `gameState` predicate over a node one wherever GameRules already publishes the value — it survives every relayout of the HUD.",
|
|
5
|
+
"scope": "scenes/main.pix3scene",
|
|
6
|
+
"params": {},
|
|
7
|
+
"uses": ["game-root", "result-overlay", "retry-button"],
|
|
8
|
+
"steps": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"name": "restart",
|
|
12
|
+
"why": "The intent RETRY dispatches, taken at the highest channel: it proves the wire without depending on where the button sits, and the dispatch is journalled with the frame it happened on."
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"expect": [
|
|
16
|
+
{ "kind": "command", "name": "restart" },
|
|
17
|
+
{ "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" },
|
|
18
|
+
{ "kind": "gameState", "path": "score", "op": "eq", "value": 0 },
|
|
19
|
+
{
|
|
20
|
+
"kind": "nodeProperty",
|
|
21
|
+
"name": "result-overlay",
|
|
22
|
+
"path": "visible",
|
|
23
|
+
"op": "eq",
|
|
24
|
+
"value": false
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|