@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.
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-7MZGPAUK.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-7WOFGLW3.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-HKG6YI6Y.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 +32 -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 +28 -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,110 +1,110 @@
1
- /**
2
- * ScoreHud — signal-driven HUD. Display only, no game logic.
3
- *
4
- * Listens on `sourceNode` (the node carrying `GameRules`) for:
5
- * `score-changed` (score)
6
- * `lives-changed` (lives, maxLives)
7
- * `time-changed` (seconds)
8
- *
9
- * If a HUD widget is missing from the scene it is quietly skipped, so you can
10
- * delete the timer or the lives bar without touching any script.
11
- *
12
- * With `GameRules.timeLimitSec` at 0 there is no clock, and `time-changed` carries
13
- * ELAPSED seconds instead — the label counts up. Set a limit and it counts down.
14
- */
15
- import { Bar2D, Label2D, Script, type PropertySchema } from '@pix3/runtime';
16
-
17
- export class ScoreHud extends Script {
18
- constructor(id: string, type: string) {
19
- super(id, type);
20
- this.config = {
21
- // Node that emits the HUD signals (the GameRules host).
22
- sourceNode: 'game-root',
23
- scoreLabel: 'score-label',
24
- scoreFormat: 'SCORE {value}',
25
- timeLabel: 'time-label',
26
- timeFormat: '{value}',
27
- livesBar: 'lives-bar',
28
- };
29
- }
30
-
31
- static getPropertySchema(): PropertySchema {
32
- const str = (name: string, label: string, description?: string) => ({
33
- name,
34
- type: 'string' as const,
35
- ui: { label, group: 'HUD', description },
36
- getValue: (s: unknown) => (s as ScoreHud).config[name],
37
- setValue: (s: unknown, v: unknown) => {
38
- (s as ScoreHud).config[name] = typeof v === 'string' ? v : '';
39
- },
40
- });
41
-
42
- return {
43
- nodeType: 'ScoreHud',
44
- properties: [
45
- str(
46
- 'sourceNode',
47
- 'Source Node',
48
- 'Node that emits score-changed / lives-changed / time-changed'
49
- ),
50
- str('scoreLabel', 'Score Label'),
51
- str('scoreFormat', 'Score Format', '{value} is replaced by the score'),
52
- str('timeLabel', 'Time Label'),
53
- str('timeFormat', 'Time Format', '{value} is replaced by whole seconds'),
54
- str('livesBar', 'Lives Bar'),
55
- ],
56
- groups: { HUD: { label: 'HUD', expanded: true } },
57
- };
58
- }
59
-
60
- onStart(): void {
61
- const source = this.findNode(String(this.config.sourceNode ?? ''));
62
- if (!source) {
63
- console.warn(`[ScoreHud] Source node "${this.config.sourceNode}" not found.`);
64
- return;
65
- }
66
- source.connect('score-changed', this, (...args: unknown[]) =>
67
- this.setScore(Number(args[0]) || 0)
68
- );
69
- source.connect('time-changed', this, (...args: unknown[]) =>
70
- this.setTime(Number(args[0]) || 0)
71
- );
72
- source.connect('lives-changed', this, (...args: unknown[]) =>
73
- this.setLives(Number(args[0]) || 0, Number(args[1]) || 0)
74
- );
75
- // Ask for the opening values now that the listeners are live: GameRules
76
- // starts before this HUD (parent components tick before children).
77
- source.emit('hud-ready');
78
- }
79
-
80
- private setScore(score: number): void {
81
- const label = this.findNode(String(this.config.scoreLabel ?? ''));
82
- if (label instanceof Label2D) {
83
- label.setText(
84
- String(this.config.scoreFormat ?? '{value}').replace('{value}', String(Math.round(score)))
85
- );
86
- }
87
- }
88
-
89
- private setTime(seconds: number): void {
90
- const label = this.findNode(String(this.config.timeLabel ?? ''));
91
- if (label instanceof Label2D) {
92
- label.setText(
93
- String(this.config.timeFormat ?? '{value}').replace(
94
- '{value}',
95
- String(Math.max(0, Math.ceil(seconds)))
96
- )
97
- );
98
- }
99
- }
100
-
101
- private setLives(lives: number, maxLives: number): void {
102
- const bar = this.findNode(String(this.config.livesBar ?? ''));
103
- if (bar instanceof Bar2D) {
104
- if (maxLives > 0) {
105
- bar.maxValue = maxLives;
106
- }
107
- bar.setValue(lives);
108
- }
109
- }
110
- }
1
+ /**
2
+ * ScoreHud — signal-driven HUD. Display only, no game logic.
3
+ *
4
+ * Listens on `sourceNode` (the node carrying `GameRules`) for:
5
+ * `score-changed` (score)
6
+ * `lives-changed` (lives, maxLives)
7
+ * `time-changed` (seconds)
8
+ *
9
+ * If a HUD widget is missing from the scene it is quietly skipped, so you can
10
+ * delete the timer or the lives bar without touching any script.
11
+ *
12
+ * With `GameRules.timeLimitSec` at 0 there is no clock, and `time-changed` carries
13
+ * ELAPSED seconds instead — the label counts up. Set a limit and it counts down.
14
+ */
15
+ import { Bar2D, Label2D, Script, type PropertySchema } from '@pix3/runtime';
16
+
17
+ export class ScoreHud extends Script {
18
+ constructor(id: string, type: string) {
19
+ super(id, type);
20
+ this.config = {
21
+ // Node that emits the HUD signals (the GameRules host).
22
+ sourceNode: 'game-root',
23
+ scoreLabel: 'score-label',
24
+ scoreFormat: 'SCORE {value}',
25
+ timeLabel: 'time-label',
26
+ timeFormat: '{value}',
27
+ livesBar: 'lives-bar',
28
+ };
29
+ }
30
+
31
+ static getPropertySchema(): PropertySchema {
32
+ const str = (name: string, label: string, description?: string) => ({
33
+ name,
34
+ type: 'string' as const,
35
+ ui: { label, group: 'HUD', description },
36
+ getValue: (s: unknown) => (s as ScoreHud).config[name],
37
+ setValue: (s: unknown, v: unknown) => {
38
+ (s as ScoreHud).config[name] = typeof v === 'string' ? v : '';
39
+ },
40
+ });
41
+
42
+ return {
43
+ nodeType: 'ScoreHud',
44
+ properties: [
45
+ str(
46
+ 'sourceNode',
47
+ 'Source Node',
48
+ 'Node that emits score-changed / lives-changed / time-changed'
49
+ ),
50
+ str('scoreLabel', 'Score Label'),
51
+ str('scoreFormat', 'Score Format', '{value} is replaced by the score'),
52
+ str('timeLabel', 'Time Label'),
53
+ str('timeFormat', 'Time Format', '{value} is replaced by whole seconds'),
54
+ str('livesBar', 'Lives Bar'),
55
+ ],
56
+ groups: { HUD: { label: 'HUD', expanded: true } },
57
+ };
58
+ }
59
+
60
+ onStart(): void {
61
+ const source = this.findNode(String(this.config.sourceNode ?? ''));
62
+ if (!source) {
63
+ console.warn(`[ScoreHud] Source node "${this.config.sourceNode}" not found.`);
64
+ return;
65
+ }
66
+ source.connect('score-changed', this, (...args: unknown[]) =>
67
+ this.setScore(Number(args[0]) || 0)
68
+ );
69
+ source.connect('time-changed', this, (...args: unknown[]) =>
70
+ this.setTime(Number(args[0]) || 0)
71
+ );
72
+ source.connect('lives-changed', this, (...args: unknown[]) =>
73
+ this.setLives(Number(args[0]) || 0, Number(args[1]) || 0)
74
+ );
75
+ // Ask for the opening values now that the listeners are live: GameRules
76
+ // starts before this HUD (parent components tick before children).
77
+ source.emit('hud-ready');
78
+ }
79
+
80
+ private setScore(score: number): void {
81
+ const label = this.findNode(String(this.config.scoreLabel ?? ''));
82
+ if (label instanceof Label2D) {
83
+ label.setText(
84
+ String(this.config.scoreFormat ?? '{value}').replace('{value}', String(Math.round(score)))
85
+ );
86
+ }
87
+ }
88
+
89
+ private setTime(seconds: number): void {
90
+ const label = this.findNode(String(this.config.timeLabel ?? ''));
91
+ if (label instanceof Label2D) {
92
+ label.setText(
93
+ String(this.config.timeFormat ?? '{value}').replace(
94
+ '{value}',
95
+ String(Math.max(0, Math.ceil(seconds)))
96
+ )
97
+ );
98
+ }
99
+ }
100
+
101
+ private setLives(lives: number, maxLives: number): void {
102
+ const bar = this.findNode(String(this.config.livesBar ?? ''));
103
+ if (bar instanceof Bar2D) {
104
+ if (maxLives > 0) {
105
+ bar.maxValue = maxLives;
106
+ }
107
+ bar.setValue(lives);
108
+ }
109
+ }
110
+ }
@@ -1,10 +1,10 @@
1
- id: recipe-blank-2d
2
- title: Blank 2D
3
- description: No mechanics on purpose — an empty field with score, lives, timer, a HUD and a win/lose overlay already wired. For an idea whose core loop is not what any other recipe ships (grid or turn-based movement, word/card/board games, builders, idle); the first increment builds the mechanic itself, controls included.
4
- projectType: 2d
5
- targetPlatform: universal
6
- viewport: { width: 1080, height: 1920 }
7
- # No `entryScene`: this recipe ships ONE scene, and the whole point is that nothing
8
- # stands between opening the project and the gameplay. Add a menu later (see
9
- # design/recipe.md → Extension points) and set it in Project Settings then.
10
- order: 64
1
+ id: recipe-blank-2d
2
+ title: Blank 2D
3
+ description: No mechanics on purpose — an empty field with score, lives, timer, a HUD and a win/lose overlay already wired. For an idea whose core loop is not what any other recipe ships (grid or turn-based movement, word/card/board games, builders, idle); the first increment builds the mechanic itself, controls included.
4
+ projectType: 2d
5
+ targetPlatform: universal
6
+ viewport: { width: 1080, height: 1920 }
7
+ # No `entryScene`: this recipe ships ONE scene, and the whole point is that nothing
8
+ # stands between opening the project and the gameplay. Add a menu later (see
9
+ # design/recipe.md → Extension points) and set it in Project Settings then.
10
+ order: 64
@@ -1,81 +1,81 @@
1
- # Bouncer 2D
2
-
3
- A playable skeleton: a ball under gravity, three bumpers, a paddle you steer,
4
- and a drain at the bottom that costs a life. It plays — and glows, pops and
5
- clicks — as shipped: press play in `scenes/main.pix3scene` before changing
6
- anything.
7
-
8
- **`design/recipe.md` is the contract.** Stable node ids, tunables with ranges,
9
- extension points (including the full pinball path), and what must not be
10
- renamed. Read it first.
11
-
12
- ## The ball physics is in this project, by choice
13
-
14
- `scripts/ball-collision.ts` implements swept (continuous) circle-vs-segment and
15
- circle-vs-circle collision with fixed substeps — the ball's whole displacement
16
- is tested each substep, so no speed can skip a wall — and `scripts/BallBody.ts`
17
- builds the colliders every frame from the **live world transforms** of ordinary
18
- marker nodes. Rotate `paddle` in the inspector, animate it from a clip, or add
19
- flippers: the physics follows with no extra code. `core:Hitbox2D` is an
20
- axis-aligned overlap test that ignores rotation — never put it on the ball.
21
-
22
- The engine also ships a general 2D rigid-body solver (`core:PhysicsBody2D` +
23
- `core:Collider2D`, `scene.physics2d`) — reach for it when the game grows bodies
24
- that push each other (stacks, debris, several balls); one ball against static
25
- geometry is exactly what the swept solver here is for, and mixing the two on the
26
- same ball is a bug.
27
-
28
- Never `import` rapier here. It is a ~2 MB lazily-loaded wasm payload for 3D
29
- only, it is not needed for this, and it would blow the export budget.
30
-
31
- **Idle / builder variant (idle-pinball, plinko-tycoon):** the drain does not
32
- have to cost a life. Set `startingLives` high or make the drain relaunch the ball
33
- (`BallBody.resetBall()` after `ball-drained`) — a conveyor returning the ball
34
- forever — and let the player spend the hits' coins on more bumpers. The paddle
35
- can be deleted or hidden; the bumpers and the juice on `ball-hit` stay.
36
-
37
- ## The neon look is three cheap ingredients
38
-
39
- 1. **`post-fx` (a `PostProcess` node with `affect2D: true`)** — bloom picks up
40
- whatever is already bright (the cyan `board-trim` strips, the bumper cores, the
41
- gold paddle, the white ball) plus a light vignette. Bloom cannot invent
42
- brightness: to make something glow, brighten its colour — don't just raise
43
- `bloomIntensity`.
44
- 2. **`bg-glow`** — `ColorRect2D` has no gradients, so the background glow is a
45
- near-white radial-gradient PNG (`sprites/ph-bg.png`) tinted with `core:tint`,
46
- sitting over the darkest `ColorRect2D`. The board floor is translucent so it
47
- reads through the playfield.
48
- 3. **`Label2D` glow** (`glowColor` / `glowStrength`) on the HUD. The HUD is a
49
- `CanvasLayer2D`, which the runtime draws *after* post-processing and never
50
- blooms — deliberately, so score text stays crisp. Its glow is canvas-drawn.
51
-
52
- Feel is engine one-liners, not code you have to write: `TouchRules` already calls
53
- `scene.audio.sfx` (procedural SFX — no audio assets in this project),
54
- `scene.juice.burst` / `floatText` / `punchScale` / `flash` / `shake` and
55
- `scene.time.hitstop` on contact, and `GameRules` plays a win/lose jingle. Spawn
56
- effects at board-space anchors: anything anchored to the HUD lands in the
57
- un-bloomed overlay band.
58
-
59
- ## Layout
60
-
61
- ```
62
- scenes/menu.pix3scene entry / export scene (menu-post-fx, glowing title)
63
- scenes/main.pix3scene the GAME (editor startup scene — iterate here)
64
- scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
65
- scripts/ball-collision.ts pure swept-collision math (unit-tested)
66
- scripts/BallBody.ts ball motion; colliders from marker world transforms
67
- scripts/PaddleController.ts input → paddle (swap this for flippers)
68
- scripts/TouchRules.ts ball-hit → score / damage + sound, particles, popups
69
- scripts/ScoreHud.ts signals → HUD widgets
70
- scripts/GameRules.ts score / lives / win / lose / end flow + jingle
71
- scripts/MenuFlow.ts menu PLAY button
72
- sprites/ph-*.png near-white placeholders (tinted via core:tint)
73
- ```
74
-
75
- Signals:
76
-
77
- ```
78
- BallBody --ball-hit(kind,nodeId,speed,x,y)--> TouchRules
79
- TouchRules --touch-scored/touch-damaged--> GameRules --score-changed--> ScoreHud
80
- --lives-changed-->
81
- ```
1
+ # Bouncer 2D
2
+
3
+ A playable skeleton: a ball under gravity, three bumpers, a paddle you steer,
4
+ and a drain at the bottom that costs a life. It plays — and glows, pops and
5
+ clicks — as shipped: press play in `scenes/main.pix3scene` before changing
6
+ anything.
7
+
8
+ **`design/recipe.md` is the contract.** Stable node ids, tunables with ranges,
9
+ extension points (including the full pinball path), and what must not be
10
+ renamed. Read it first.
11
+
12
+ ## The ball physics is in this project, by choice
13
+
14
+ `scripts/ball-collision.ts` implements swept (continuous) circle-vs-segment and
15
+ circle-vs-circle collision with fixed substeps — the ball's whole displacement
16
+ is tested each substep, so no speed can skip a wall — and `scripts/BallBody.ts`
17
+ builds the colliders every frame from the **live world transforms** of ordinary
18
+ marker nodes. Rotate `paddle` in the inspector, animate it from a clip, or add
19
+ flippers: the physics follows with no extra code. `core:Hitbox2D` is an
20
+ axis-aligned overlap test that ignores rotation — never put it on the ball.
21
+
22
+ The engine also ships a general 2D rigid-body solver (`core:PhysicsBody2D` +
23
+ `core:Collider2D`, `scene.physics2d`) — reach for it when the game grows bodies
24
+ that push each other (stacks, debris, several balls); one ball against static
25
+ geometry is exactly what the swept solver here is for, and mixing the two on the
26
+ same ball is a bug.
27
+
28
+ Never `import` rapier here. It is a ~2 MB lazily-loaded wasm payload for 3D
29
+ only, it is not needed for this, and it would blow the export budget.
30
+
31
+ **Idle / builder variant (idle-pinball, plinko-tycoon):** the drain does not
32
+ have to cost a life. Set `startingLives` high or make the drain relaunch the ball
33
+ (`BallBody.resetBall()` after `ball-drained`) — a conveyor returning the ball
34
+ forever — and let the player spend the hits' coins on more bumpers. The paddle
35
+ can be deleted or hidden; the bumpers and the juice on `ball-hit` stay.
36
+
37
+ ## The neon look is three cheap ingredients
38
+
39
+ 1. **`post-fx` (a `PostProcess` node with `affect2D: true`)** — bloom picks up
40
+ whatever is already bright (the cyan `board-trim` strips, the bumper cores, the
41
+ gold paddle, the white ball) plus a light vignette. Bloom cannot invent
42
+ brightness: to make something glow, brighten its colour — don't just raise
43
+ `bloomIntensity`.
44
+ 2. **`bg-glow`** — `ColorRect2D` has no gradients, so the background glow is a
45
+ near-white radial-gradient PNG (`sprites/ph-bg.png`) tinted with `core:tint`,
46
+ sitting over the darkest `ColorRect2D`. The board floor is translucent so it
47
+ reads through the playfield.
48
+ 3. **`Label2D` glow** (`glowColor` / `glowStrength`) on the HUD. The HUD is a
49
+ `CanvasLayer2D`, which the runtime draws *after* post-processing and never
50
+ blooms — deliberately, so score text stays crisp. Its glow is canvas-drawn.
51
+
52
+ Feel is engine one-liners, not code you have to write: `TouchRules` already calls
53
+ `scene.audio.sfx` (procedural SFX — no audio assets in this project),
54
+ `scene.juice.burst` / `floatText` / `punchScale` / `flash` / `shake` and
55
+ `scene.time.hitstop` on contact, and `GameRules` plays a win/lose jingle. Spawn
56
+ effects at board-space anchors: anything anchored to the HUD lands in the
57
+ un-bloomed overlay band.
58
+
59
+ ## Layout
60
+
61
+ ```
62
+ scenes/menu.pix3scene entry / export scene (menu-post-fx, glowing title)
63
+ scenes/main.pix3scene the GAME (editor startup scene — iterate here)
64
+ scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
65
+ scripts/ball-collision.ts pure swept-collision math (unit-tested)
66
+ scripts/BallBody.ts ball motion; colliders from marker world transforms
67
+ scripts/PaddleController.ts input → paddle (swap this for flippers)
68
+ scripts/TouchRules.ts ball-hit → score / damage + sound, particles, popups
69
+ scripts/ScoreHud.ts signals → HUD widgets
70
+ scripts/GameRules.ts score / lives / win / lose / end flow + jingle
71
+ scripts/MenuFlow.ts menu PLAY button
72
+ sprites/ph-*.png near-white placeholders (tinted via core:tint)
73
+ ```
74
+
75
+ Signals:
76
+
77
+ ```
78
+ BallBody --ball-hit(kind,nodeId,speed,x,y)--> TouchRules
79
+ TouchRules --touch-scored/touch-damaged--> GameRules --score-changed--> ScoreHud
80
+ --lives-changed-->
81
+ ```