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