@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.
Files changed (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-CBFYKDS7.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-CFQFEI52.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +39 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +43 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
@@ -1,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`.