@pix3/cli 1.6.0 → 1.6.3
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-CBFYKDS7.mjs} +1 -1
- package/dist/smoke/prebuilt/chunk-VJHJ5BCO.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-CFQFEI52.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 +39 -24
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +43 -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,146 +1,146 @@
|
|
|
1
|
-
# Recipe: recipe-bouncer-2d
|
|
2
|
-
|
|
3
|
-
## What this is
|
|
4
|
-
|
|
5
|
-
A ball under gravity bounces off walls, bumpers and a paddle you steer; hits score, the
|
|
6
|
-
bottom drain costs a life. Playable, neon-lit and audible before any edit.
|
|
7
|
-
|
|
8
|
-
Ball physics lives in the project: `BallBody` runs the swept solver (`ball-collision.ts`)
|
|
9
|
-
over colliders rebuilt each frame from the marker nodes' **live world transforms**, so
|
|
10
|
-
a rotated flipper works for free. No `core:Hitbox2D` on the ball, no rapier. (The
|
|
11
|
-
engine's own `scene.physics2d` / `core:PhysicsBody2D` solver exists for games that grow
|
|
12
|
-
many interacting bodies — do not put both on the same ball.)
|
|
13
|
-
|
|
14
|
-
The paddle is optional: for an idle pinball, hide it and make the drain relaunch
|
|
15
|
-
instead of costing a life — the bumpers and the `ball-hit` juice carry the game.
|
|
16
|
-
|
|
17
|
-
The look: `PostProcess` (bloom + vignette) over bright accents, a tinted gradient
|
|
18
|
-
sprite behind the board, `Label2D` glow on the HUD (drawn after post — it glows on
|
|
19
|
-
canvas, never blooms).
|
|
20
|
-
|
|
21
|
-
## Node map
|
|
22
|
-
|
|
23
|
-
| id | what |
|
|
24
|
-
| --- | --- |
|
|
25
|
-
| `post-fx` | `PostProcess` (`affect2D`): bloom + vignette |
|
|
26
|
-
| `game-root` | root; hosts `GameRules` + `TouchRules`; emits the HUD signals |
|
|
27
|
-
| `game-background` | full-screen `ColorRect2D`, the darkest colour |
|
|
28
|
-
| `bg-glow` | full-screen `Sprite2D`, the tinted gradient over it |
|
|
29
|
-
| `board` | playfield `Group2D`; clamps the paddle |
|
|
30
|
-
| `board-floor` | the plate, translucent so the glow shows through |
|
|
31
|
-
| `board-trim` | `trim-left`/`-right`/`-top` + `trim-drain`: DECOR, not colliders |
|
|
32
|
-
| `walls` → `wall-left`/`-right`/`-top` | box colliders, kind `wall` |
|
|
33
|
-
| `bumpers` → `bumper-a`/`-b`/`-c` | circle colliders, kind `bumper`; each a `Group2D` of `*-halo` + `*-core` rings |
|
|
34
|
-
| `paddles` → `paddle` | box collider, kind `paddle`; `PaddleController` |
|
|
35
|
-
| `drain` | trigger box, kind `drain` — entering it loses a ball |
|
|
36
|
-
| `ball` | hosts `BallBody` |
|
|
37
|
-
| `hud` | `CanvasLayer2D` + `ScoreHud`: `score-label`, `time-label`, `lives-bar`, `menu-button` |
|
|
38
|
-
| `result-overlay` / `result-label` / `retry-button` | end screen in `scenes/ui/result.pix3scene`, instanced into main hidden (`visible: false` is editor-only) |
|
|
39
|
-
|
|
40
|
-
`menu.pix3scene`: `menu-post-fx`, `menu-root`, `menu-background`, `menu-bg-glow`,
|
|
41
|
-
`title-label` (title patched in), `title-rule`, `subtitle-label`, `play-button`.
|
|
42
|
-
|
|
43
|
-
Signals: `ball` emits `ball-hit` (kind, nodeId, speed, x, y — world) and `ball-drained`;
|
|
44
|
-
`TouchRules` re-emits `touch-scored` / `touch-damaged` on `game-root`, where `GameRules`
|
|
45
|
-
answers `score-changed`, `lives-changed`, `time-changed`, `game-won` / `game-lost`.
|
|
46
|
-
|
|
47
|
-
## Placeholders
|
|
48
|
-
|
|
49
|
-
Near-white PNGs carrying a `core:tint`: set `effects[0].params.color`.
|
|
50
|
-
|
|
51
|
-
| role | file | node |
|
|
52
|
-
| --- | --- | --- |
|
|
53
|
-
| background | `sprites/ph-bg.png` | `bg-glow`, `menu-bg-glow` (radial gradient) |
|
|
54
|
-
| avatar | `sprites/ph-ball.png` | `ball` |
|
|
55
|
-
| player | `sprites/ph-paddle.png` | `paddle` |
|
|
56
|
-
| collectible | `sprites/ph-bumper.png` | `bumper-*-core` / `*-halo` (a ring) |
|
|
57
|
-
|
|
58
|
-
## Tunables
|
|
59
|
-
|
|
60
|
-
```yaml
|
|
61
|
-
tunables:
|
|
62
|
-
gravity: { node: ball, component: "user:BallBody", property: gravity }
|
|
63
|
-
maxSpeed: { node: ball, component: "user:BallBody", property: maxSpeed }
|
|
64
|
-
minSpeed: { node: ball, component: "user:BallBody", property: minSpeed }
|
|
65
|
-
ballRadius: { node: ball, component: "user:BallBody", property: radius }
|
|
66
|
-
launchSpeed: { node: ball, component: "user:BallBody", property: launchSpeed }
|
|
67
|
-
launchAngleDeg: { node: ball, component: "user:BallBody", property: launchAngleDeg }
|
|
68
|
-
wallBounce: { node: ball, component: "user:BallBody", property: wallRestitution }
|
|
69
|
-
paddleBounce: { node: ball, component: "user:BallBody", property: paddleRestitution }
|
|
70
|
-
bumperBounce: { node: ball, component: "user:BallBody", property: bumperRestitution }
|
|
71
|
-
paddleMode: { node: paddle, component: "user:PaddleController", property: mode, default: pointer }
|
|
72
|
-
paddleSpeed: { node: paddle, component: "user:PaddleController", property: speed }
|
|
73
|
-
paddleHalfWidth: { node: paddle, component: "user:PaddleController", property: halfWidth }
|
|
74
|
-
targetScore: { node: game-root, component: "user:GameRules", property: targetScore }
|
|
75
|
-
startingLives: { node: game-root, component: "user:GameRules", property: startingLives }
|
|
76
|
-
touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "bumper:score:100;paddle:score:10;drain:damage:1" }
|
|
77
|
-
punchAmount: { node: game-root, component: "user:TouchRules", property: punchAmount }
|
|
78
|
-
hitstopMs: { node: game-root, component: "user:TouchRules", property: hitstopMs }
|
|
79
|
-
burstCount: { node: game-root, component: "user:TouchRules", property: burstCount }
|
|
80
|
-
sfxEnabled: { node: game-root, component: "user:TouchRules", property: sfxEnabled }
|
|
81
|
-
bgColor: { node: game-background, property: color, default: "#05030f" }
|
|
82
|
-
boardColor: { node: board-floor, property: color, default: "#120a33" }
|
|
83
|
-
bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.9 }
|
|
84
|
-
menuBloomIntensity: { node: menu-post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.7 }
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
`component` present → `set_component_property`, absent → `set_property`. Current values
|
|
88
|
-
live in the scene; each script's schema clamps on write. Bloom lifts only what is
|
|
89
|
-
*already* bright — brighten the colour or lower `bloomThreshold` (0.58), don't raise
|
|
90
|
-
intensity. A pastel / light ground is the reverse: **raise** it (~0.9) or everything blooms.
|
|
91
|
-
|
|
92
|
-
## Extension points
|
|
93
|
-
|
|
94
|
-
- **Pinball flippers.** Two boxes **directly** under `paddles` (`flipper-left`,
|
|
95
|
-
`flipper-right`); swap `PaddleController` for a flipper script. `BallBody` reads each
|
|
96
|
-
direct child as a box centred on the node (a pivot `Group2D` there would itself be
|
|
97
|
-
the collider), so pivot in code: fix each flipper's **outer** end near its wall, tip
|
|
98
|
-
toward the centre, and every frame put the centre at `pivot ± (width/2)(cos θ, sin θ)`.
|
|
99
|
-
Holding `Key_ArrowLeft`/`Key_ArrowRight` lerps `rotation.z` −25° → +35° in ~0.06 s
|
|
100
|
-
(the right flipper mirrored: +25° → −35°); release lerps back. Add inlane guides
|
|
101
|
-
(boxes under `walls` from each side wall to a flipper's outer end), or the ball
|
|
102
|
-
drains around the flippers.
|
|
103
|
-
- **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
|
|
104
|
-
`scene.audio.sfx('score')`, `scene.juice.burst({x, y})`,
|
|
105
|
-
`floatText('+100', { at: node })`, `punchScale`/`shake`/`flash`,
|
|
106
|
-
`scene.time.hitstop(50)` (on contact START, not every frame),
|
|
107
|
-
`core:BurstOnSignal`/`core:SfxOnSignal`. `TouchRules` already calls them on
|
|
108
|
-
contact — copy the line next to your mechanic, in board space.
|
|
109
|
-
- **More bumpers.** Duplicate a `bumpers` child (radius = width/2 × world scale) —
|
|
110
|
-
punch, particles and score already ride `ball-hit`.
|
|
111
|
-
- **Brick field (arkanoid).** Bricks under `walls` with ids `brick-*`; in `TouchRules`
|
|
112
|
-
`queueFree` the hit node when `kind === 'wall'` and the id matches.
|
|
113
|
-
|
|
114
|
-
Replacing a script named above is expected — `fs_write` with `overwrite: true` + `reason`.
|
|
115
|
-
|
|
116
|
-
## Do not touch
|
|
117
|
-
|
|
118
|
-
- **Ending a run is `GameRules`' job**: `finish(won)` shows `result-overlay`, plays the
|
|
119
|
-
jingle, enables the initially-disabled `retry-button` and freezes gameplay. Call it
|
|
120
|
-
for custom endings — showing the overlay yourself leaves RETRY unpressable.
|
|
121
|
-
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
122
|
-
- The collider grouping: `walls`/`paddles` children are boxes, `bumpers` children
|
|
123
|
-
circles, `drain` a trigger. Moving a node between them changes its physics, and a
|
|
124
|
-
decoration dropped into one becomes a collider.
|
|
125
|
-
- `post-fx` / `menu-post-fx`: keep the ids and `affect2D: true` — without it a 2D-only
|
|
126
|
-
scene gets no effects. Tune the values, don't delete the nodes.
|
|
127
|
-
- No `core:Hitbox2D` on the ball or walls (axis-aligned overlap would disagree with the
|
|
128
|
-
swept solver), and no rapier.
|
|
129
|
-
|
|
130
|
-
## Playtest contract
|
|
131
|
-
|
|
132
|
-
terminalVisual: static
|
|
133
|
-
terminalRestartRoutine: terminal-retry
|
|
134
|
-
|
|
135
|
-
## Verify
|
|
136
|
-
|
|
137
|
-
1. `play_start` on `scenes/main.pix3scene`.
|
|
138
|
-
2. `game_observe` twice ~0.5 s apart: `ball` moves and stays inside the board
|
|
139
|
-
(|x| < 470, y > −800 board space).
|
|
140
|
-
3. **The core promise: the defender keeps the ball in play.** Whatever guards the
|
|
141
|
-
drain (paddle, flippers, …) must cover or funnel every downward path, never let the
|
|
142
|
-
ball round its open sides. Prove a save with `game_input`: a driven run outlives an
|
|
143
|
-
unattended one, and an unattended one survives several seconds (the stage idles
|
|
144
|
-
between turns, so a board that loses itself instantly reads as broken).
|
|
145
|
-
4. Let the ball drain: `lives-bar` drops one and it relaunches after `resetDelaySec`;
|
|
146
|
-
three drains → `result-overlay` with `GAME OVER`.
|
|
1
|
+
# Recipe: recipe-bouncer-2d
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
A ball under gravity bounces off walls, bumpers and a paddle you steer; hits score, the
|
|
6
|
+
bottom drain costs a life. Playable, neon-lit and audible before any edit.
|
|
7
|
+
|
|
8
|
+
Ball physics lives in the project: `BallBody` runs the swept solver (`ball-collision.ts`)
|
|
9
|
+
over colliders rebuilt each frame from the marker nodes' **live world transforms**, so
|
|
10
|
+
a rotated flipper works for free. No `core:Hitbox2D` on the ball, no rapier. (The
|
|
11
|
+
engine's own `scene.physics2d` / `core:PhysicsBody2D` solver exists for games that grow
|
|
12
|
+
many interacting bodies — do not put both on the same ball.)
|
|
13
|
+
|
|
14
|
+
The paddle is optional: for an idle pinball, hide it and make the drain relaunch
|
|
15
|
+
instead of costing a life — the bumpers and the `ball-hit` juice carry the game.
|
|
16
|
+
|
|
17
|
+
The look: `PostProcess` (bloom + vignette) over bright accents, a tinted gradient
|
|
18
|
+
sprite behind the board, `Label2D` glow on the HUD (drawn after post — it glows on
|
|
19
|
+
canvas, never blooms).
|
|
20
|
+
|
|
21
|
+
## Node map
|
|
22
|
+
|
|
23
|
+
| id | what |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `post-fx` | `PostProcess` (`affect2D`): bloom + vignette |
|
|
26
|
+
| `game-root` | root; hosts `GameRules` + `TouchRules`; emits the HUD signals |
|
|
27
|
+
| `game-background` | full-screen `ColorRect2D`, the darkest colour |
|
|
28
|
+
| `bg-glow` | full-screen `Sprite2D`, the tinted gradient over it |
|
|
29
|
+
| `board` | playfield `Group2D`; clamps the paddle |
|
|
30
|
+
| `board-floor` | the plate, translucent so the glow shows through |
|
|
31
|
+
| `board-trim` | `trim-left`/`-right`/`-top` + `trim-drain`: DECOR, not colliders |
|
|
32
|
+
| `walls` → `wall-left`/`-right`/`-top` | box colliders, kind `wall` |
|
|
33
|
+
| `bumpers` → `bumper-a`/`-b`/`-c` | circle colliders, kind `bumper`; each a `Group2D` of `*-halo` + `*-core` rings |
|
|
34
|
+
| `paddles` → `paddle` | box collider, kind `paddle`; `PaddleController` |
|
|
35
|
+
| `drain` | trigger box, kind `drain` — entering it loses a ball |
|
|
36
|
+
| `ball` | hosts `BallBody` |
|
|
37
|
+
| `hud` | `CanvasLayer2D` + `ScoreHud`: `score-label`, `time-label`, `lives-bar`, `menu-button` |
|
|
38
|
+
| `result-overlay` / `result-label` / `retry-button` | end screen in `scenes/ui/result.pix3scene`, instanced into main hidden (`visible: false` is editor-only) |
|
|
39
|
+
|
|
40
|
+
`menu.pix3scene`: `menu-post-fx`, `menu-root`, `menu-background`, `menu-bg-glow`,
|
|
41
|
+
`title-label` (title patched in), `title-rule`, `subtitle-label`, `play-button`.
|
|
42
|
+
|
|
43
|
+
Signals: `ball` emits `ball-hit` (kind, nodeId, speed, x, y — world) and `ball-drained`;
|
|
44
|
+
`TouchRules` re-emits `touch-scored` / `touch-damaged` on `game-root`, where `GameRules`
|
|
45
|
+
answers `score-changed`, `lives-changed`, `time-changed`, `game-won` / `game-lost`.
|
|
46
|
+
|
|
47
|
+
## Placeholders
|
|
48
|
+
|
|
49
|
+
Near-white PNGs carrying a `core:tint`: set `effects[0].params.color`.
|
|
50
|
+
|
|
51
|
+
| role | file | node |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| background | `sprites/ph-bg.png` | `bg-glow`, `menu-bg-glow` (radial gradient) |
|
|
54
|
+
| avatar | `sprites/ph-ball.png` | `ball` |
|
|
55
|
+
| player | `sprites/ph-paddle.png` | `paddle` |
|
|
56
|
+
| collectible | `sprites/ph-bumper.png` | `bumper-*-core` / `*-halo` (a ring) |
|
|
57
|
+
|
|
58
|
+
## Tunables
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
tunables:
|
|
62
|
+
gravity: { node: ball, component: "user:BallBody", property: gravity }
|
|
63
|
+
maxSpeed: { node: ball, component: "user:BallBody", property: maxSpeed }
|
|
64
|
+
minSpeed: { node: ball, component: "user:BallBody", property: minSpeed }
|
|
65
|
+
ballRadius: { node: ball, component: "user:BallBody", property: radius }
|
|
66
|
+
launchSpeed: { node: ball, component: "user:BallBody", property: launchSpeed }
|
|
67
|
+
launchAngleDeg: { node: ball, component: "user:BallBody", property: launchAngleDeg }
|
|
68
|
+
wallBounce: { node: ball, component: "user:BallBody", property: wallRestitution }
|
|
69
|
+
paddleBounce: { node: ball, component: "user:BallBody", property: paddleRestitution }
|
|
70
|
+
bumperBounce: { node: ball, component: "user:BallBody", property: bumperRestitution }
|
|
71
|
+
paddleMode: { node: paddle, component: "user:PaddleController", property: mode, default: pointer }
|
|
72
|
+
paddleSpeed: { node: paddle, component: "user:PaddleController", property: speed }
|
|
73
|
+
paddleHalfWidth: { node: paddle, component: "user:PaddleController", property: halfWidth }
|
|
74
|
+
targetScore: { node: game-root, component: "user:GameRules", property: targetScore }
|
|
75
|
+
startingLives: { node: game-root, component: "user:GameRules", property: startingLives }
|
|
76
|
+
touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "bumper:score:100;paddle:score:10;drain:damage:1" }
|
|
77
|
+
punchAmount: { node: game-root, component: "user:TouchRules", property: punchAmount }
|
|
78
|
+
hitstopMs: { node: game-root, component: "user:TouchRules", property: hitstopMs }
|
|
79
|
+
burstCount: { node: game-root, component: "user:TouchRules", property: burstCount }
|
|
80
|
+
sfxEnabled: { node: game-root, component: "user:TouchRules", property: sfxEnabled }
|
|
81
|
+
bgColor: { node: game-background, property: color, default: "#05030f" }
|
|
82
|
+
boardColor: { node: board-floor, property: color, default: "#120a33" }
|
|
83
|
+
bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.9 }
|
|
84
|
+
menuBloomIntensity: { node: menu-post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.7 }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`component` present → `set_component_property`, absent → `set_property`. Current values
|
|
88
|
+
live in the scene; each script's schema clamps on write. Bloom lifts only what is
|
|
89
|
+
*already* bright — brighten the colour or lower `bloomThreshold` (0.58), don't raise
|
|
90
|
+
intensity. A pastel / light ground is the reverse: **raise** it (~0.9) or everything blooms.
|
|
91
|
+
|
|
92
|
+
## Extension points
|
|
93
|
+
|
|
94
|
+
- **Pinball flippers.** Two boxes **directly** under `paddles` (`flipper-left`,
|
|
95
|
+
`flipper-right`); swap `PaddleController` for a flipper script. `BallBody` reads each
|
|
96
|
+
direct child as a box centred on the node (a pivot `Group2D` there would itself be
|
|
97
|
+
the collider), so pivot in code: fix each flipper's **outer** end near its wall, tip
|
|
98
|
+
toward the centre, and every frame put the centre at `pivot ± (width/2)(cos θ, sin θ)`.
|
|
99
|
+
Holding `Key_ArrowLeft`/`Key_ArrowRight` lerps `rotation.z` −25° → +35° in ~0.06 s
|
|
100
|
+
(the right flipper mirrored: +25° → −35°); release lerps back. Add inlane guides
|
|
101
|
+
(boxes under `walls` from each side wall to a flipper's outer end), or the ball
|
|
102
|
+
drains around the flippers.
|
|
103
|
+
- **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
|
|
104
|
+
`scene.audio.sfx('score')`, `scene.juice.burst({x, y})`,
|
|
105
|
+
`floatText('+100', { at: node })`, `punchScale`/`shake`/`flash`,
|
|
106
|
+
`scene.time.hitstop(50)` (on contact START, not every frame),
|
|
107
|
+
`core:BurstOnSignal`/`core:SfxOnSignal`. `TouchRules` already calls them on
|
|
108
|
+
contact — copy the line next to your mechanic, in board space.
|
|
109
|
+
- **More bumpers.** Duplicate a `bumpers` child (radius = width/2 × world scale) —
|
|
110
|
+
punch, particles and score already ride `ball-hit`.
|
|
111
|
+
- **Brick field (arkanoid).** Bricks under `walls` with ids `brick-*`; in `TouchRules`
|
|
112
|
+
`queueFree` the hit node when `kind === 'wall'` and the id matches.
|
|
113
|
+
|
|
114
|
+
Replacing a script named above is expected — `fs_write` with `overwrite: true` + `reason`.
|
|
115
|
+
|
|
116
|
+
## Do not touch
|
|
117
|
+
|
|
118
|
+
- **Ending a run is `GameRules`' job**: `finish(won)` shows `result-overlay`, plays the
|
|
119
|
+
jingle, enables the initially-disabled `retry-button` and freezes gameplay. Call it
|
|
120
|
+
for custom endings — showing the overlay yourself leaves RETRY unpressable.
|
|
121
|
+
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
122
|
+
- The collider grouping: `walls`/`paddles` children are boxes, `bumpers` children
|
|
123
|
+
circles, `drain` a trigger. Moving a node between them changes its physics, and a
|
|
124
|
+
decoration dropped into one becomes a collider.
|
|
125
|
+
- `post-fx` / `menu-post-fx`: keep the ids and `affect2D: true` — without it a 2D-only
|
|
126
|
+
scene gets no effects. Tune the values, don't delete the nodes.
|
|
127
|
+
- No `core:Hitbox2D` on the ball or walls (axis-aligned overlap would disagree with the
|
|
128
|
+
swept solver), and no rapier.
|
|
129
|
+
|
|
130
|
+
## Playtest contract
|
|
131
|
+
|
|
132
|
+
terminalVisual: static
|
|
133
|
+
terminalRestartRoutine: terminal-retry
|
|
134
|
+
|
|
135
|
+
## Verify
|
|
136
|
+
|
|
137
|
+
1. `play_start` on `scenes/main.pix3scene`.
|
|
138
|
+
2. `game_observe` twice ~0.5 s apart: `ball` moves and stays inside the board
|
|
139
|
+
(|x| < 470, y > −800 board space).
|
|
140
|
+
3. **The core promise: the defender keeps the ball in play.** Whatever guards the
|
|
141
|
+
drain (paddle, flippers, …) must cover or funnel every downward path, never let the
|
|
142
|
+
ball round its open sides. Prove a save with `game_input`: a driven run outlives an
|
|
143
|
+
unattended one, and an unattended one survives several seconds (the stage idles
|
|
144
|
+
between turns, so a board that loses itself instantly reads as broken).
|
|
145
|
+
4. Let the ball drain: `lives-bar` drops one and it relaunches after `resetDelaySec`;
|
|
146
|
+
three drains → `result-overlay` with `GAME OVER`.
|