@pix3/cli 1.6.4 → 2.0.0-alpha.0
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 +722 -788
- package/dist/index.js +557 -421
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
- package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
- package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
- package/dist/validate/prebuilt/bundle-main.js +223 -18
- package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
- package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
- package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
- package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
- package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
- package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
- package/kit/files/AGENTS.md +76 -84
- package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
- package/kit/kit.json +8 -5
- package/package.json +62 -54
- package/templates/2d/files/scenes/main.pix3scene +31 -0
- package/templates/2d/template.yaml +11 -0
- package/templates/3d/files/scenes/main.pix3scene +50 -0
- package/templates/3d/template.yaml +11 -0
- package/templates/base/files/README.md +39 -0
- package/templates/base/files/gitignore +11 -0
- package/templates/base/files/index.html +23 -0
- package/templates/base/files/package.json +28 -0
- package/templates/base/files/src/main.ts +3 -0
- package/templates/base/files/tsconfig.json +17 -0
- package/templates/base/files/vite.config.ts +10 -0
- package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
- package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
- package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
- package/templates/empty-2d/files/README.md +0 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
- package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-2d/template.yaml +0 -7
- package/templates/empty-3d/files/README.md +0 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
- package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-3d/template.yaml +0 -7
- package/templates/idea-blank/files/README.md +0 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
- package/templates/idea-blank/template.yaml +0 -10
- package/templates/minigame-2d/files/README.md +0 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
- package/templates/minigame-2d/template.yaml +0 -10
- package/templates/playable-2d/files/README.md +0 -30
- package/templates/playable-2d/files/design/recipe.md +0 -83
- package/templates/playable-2d/files/design/tests/reachability.json +0 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
- package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/playable-2d/template.yaml +0 -9
- package/templates/playable-3d/files/README.md +0 -32
- package/templates/playable-3d/files/design/recipe.md +0 -127
- package/templates/playable-3d/files/design/tests/reachability.json +0 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
- package/templates/playable-3d/template.yaml +0 -9
- package/templates/recipe-arena-2d/files/README.md +0 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
- package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
- package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
- package/templates/recipe-arena-2d/template.yaml +0 -10
- package/templates/recipe-blank-2d/files/README.md +0 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
- package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
- package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
- package/templates/recipe-blank-2d/template.yaml +0 -10
- package/templates/recipe-bouncer-2d/files/README.md +0 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
- package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
- package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
- package/templates/recipe-bouncer-2d/template.yaml +0 -10
- package/templates/recipe-grid-3d/files/README.md +0 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-grid-3d/template.yaml +0 -10
- package/templates/recipe-tapper-2d/files/README.md +0 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
- package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
- package/templates/recipe-tapper-2d/template.yaml +0 -10
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# Pix3 Scene File (YAML)
|
|
2
|
-
# Blank 2D — the RESULT overlay (win/lose), in its own file so the gameplay
|
|
3
|
-
# scene opens clean. `main.pix3scene` carries it as a one-line
|
|
4
|
-
# `instance:` reference marked `visible: false`, which hides it in the EDITOR
|
|
5
|
-
# only; `user:GameRules` reveals it at run time by setting `visible`.
|
|
6
|
-
#
|
|
7
|
-
# Edit it here, full-screen, in its own tab. To preview it composited over the
|
|
8
|
-
# field, tick the eye on `result-overlay` in main.pix3scene — just do not commit
|
|
9
|
-
# that tick, or every iteration starts behind a GAME OVER screen again.
|
|
10
|
-
#
|
|
11
|
-
# Stable node ids (design/recipe.md is the contract — do not rename these):
|
|
12
|
-
# result-overlay, result-dim, result-label, retry-button
|
|
13
|
-
version: 1.0.0
|
|
14
|
-
metadata:
|
|
15
|
-
author: Pix3 Recipe
|
|
16
|
-
description: Result overlay — dim, win/lose headline and a RETRY button
|
|
17
|
-
root:
|
|
18
|
-
- id: result-overlay
|
|
19
|
-
type: Group2D
|
|
20
|
-
name: Result Overlay
|
|
21
|
-
properties:
|
|
22
|
-
width: 1080
|
|
23
|
-
height: 1920
|
|
24
|
-
initiallyVisible: false
|
|
25
|
-
layout:
|
|
26
|
-
enabled: true
|
|
27
|
-
horizontalAlign: stretch
|
|
28
|
-
verticalAlign: stretch
|
|
29
|
-
children:
|
|
30
|
-
- id: result-dim
|
|
31
|
-
type: ColorRect2D
|
|
32
|
-
name: Result Dim
|
|
33
|
-
properties:
|
|
34
|
-
width: 1080
|
|
35
|
-
height: 1920
|
|
36
|
-
color: "#000000"
|
|
37
|
-
opacity: 0.7
|
|
38
|
-
transform:
|
|
39
|
-
position: [0, 0]
|
|
40
|
-
scale: [1, 1]
|
|
41
|
-
rotation: 0
|
|
42
|
-
layout:
|
|
43
|
-
enabled: true
|
|
44
|
-
horizontalAlign: stretch
|
|
45
|
-
verticalAlign: stretch
|
|
46
|
-
children: []
|
|
47
|
-
- id: result-label
|
|
48
|
-
type: Label2D
|
|
49
|
-
name: Result Label
|
|
50
|
-
properties:
|
|
51
|
-
label: "GAME OVER"
|
|
52
|
-
labelFontSize: 88
|
|
53
|
-
labelColor: "#ffffff"
|
|
54
|
-
labelAlign: center
|
|
55
|
-
transform:
|
|
56
|
-
position: [0, 160]
|
|
57
|
-
scale: [1, 1]
|
|
58
|
-
rotation: 0
|
|
59
|
-
layout:
|
|
60
|
-
enabled: true
|
|
61
|
-
horizontalAlign: center
|
|
62
|
-
verticalAlign: center
|
|
63
|
-
children: []
|
|
64
|
-
- id: retry-button
|
|
65
|
-
type: Button2D
|
|
66
|
-
name: Retry Button
|
|
67
|
-
properties:
|
|
68
|
-
width: 420
|
|
69
|
-
height: 130
|
|
70
|
-
label: "RETRY"
|
|
71
|
-
labelFontSize: 50
|
|
72
|
-
labelColor: "#12141c"
|
|
73
|
-
backgroundColor: "#f5ae39"
|
|
74
|
-
hoverColor: "#ffc862"
|
|
75
|
-
pressedColor: "#d4922a"
|
|
76
|
-
buttonAction: retry
|
|
77
|
-
transform:
|
|
78
|
-
position: [0, -60]
|
|
79
|
-
scale: [1, 1]
|
|
80
|
-
rotation: 0
|
|
81
|
-
layout:
|
|
82
|
-
enabled: true
|
|
83
|
-
horizontalAlign: center
|
|
84
|
-
verticalAlign: center
|
|
85
|
-
children: []
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GameRules — score, lives, timer, win/lose and the end flow.
|
|
3
|
-
*
|
|
4
|
-
* This is the blank recipe's only piece of game logic, and it is deliberately
|
|
5
|
-
* about the *outcome* of a run, never about its mechanic. It owns:
|
|
6
|
-
* - the run state (score, lives, clock, outcome),
|
|
7
|
-
* - the HUD signals (`score-changed`, `lives-changed`, `time-changed`),
|
|
8
|
-
* - the result overlay and RETRY,
|
|
9
|
-
* - the `restart` intent,
|
|
10
|
-
* - the debug snapshot every verification tool reads.
|
|
11
|
-
*
|
|
12
|
-
* Your mechanic talks to it in either of two ways, whichever suits the script:
|
|
13
|
-
* 1. call the methods — `rules.addScore(1)`, `rules.loseLife()`, `rules.finish(true)`;
|
|
14
|
-
* 2. emit on the node that carries this component — `node.emit('score-added', 1)`
|
|
15
|
-
* or `node.emit('life-lost', 1)` — which needs no reference at all.
|
|
16
|
-
*
|
|
17
|
-
* Win modes:
|
|
18
|
-
* `score` — reach `targetScore`; a non-zero time limit is a deadline.
|
|
19
|
-
* `time` — play until the clock ends, then win if `targetScore` was reached.
|
|
20
|
-
* `survive` — stay alive until the clock ends; reaching it is the win.
|
|
21
|
-
* In every mode, running out of lives loses. `timeLimitSec: 0` means no clock,
|
|
22
|
-
* so the HUD's time label counts elapsed seconds up instead of down.
|
|
23
|
-
*
|
|
24
|
-
* RETRY dispatches the `restart` command (`scene.commands`) rather than calling a
|
|
25
|
-
* method: a test replays the end flow without hunting for buttons, and one real
|
|
26
|
-
* tap proves the binding once.
|
|
27
|
-
*/
|
|
28
|
-
import {
|
|
29
|
-
Button2D,
|
|
30
|
-
Label2D,
|
|
31
|
-
NodeBase,
|
|
32
|
-
Script,
|
|
33
|
-
registerGameDebug,
|
|
34
|
-
type PropertySchema,
|
|
35
|
-
} from '@pix3/runtime';
|
|
36
|
-
|
|
37
|
-
type WinMode = 'score' | 'time' | 'survive';
|
|
38
|
-
|
|
39
|
-
/** Snapshot numbers are read by humans and diffed by tools — keep them short. */
|
|
40
|
-
function roundHundredths(value: number): number {
|
|
41
|
-
return Math.round(value * 100) / 100;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class GameRules extends Script {
|
|
45
|
-
private score = 0;
|
|
46
|
-
private lives = 3;
|
|
47
|
-
private elapsed = 0;
|
|
48
|
-
private over = false;
|
|
49
|
-
private won = false;
|
|
50
|
-
private disposeDebug: (() => void) | null = null;
|
|
51
|
-
private disposeCommands: (() => void)[] = [];
|
|
52
|
-
/** Components `finish()` switched off, so `resetRun()` revives exactly those. */
|
|
53
|
-
private frozen: { enabled: boolean }[] = [];
|
|
54
|
-
|
|
55
|
-
constructor(id: string, type: string) {
|
|
56
|
-
super(id, type);
|
|
57
|
-
this.config = {
|
|
58
|
-
winMode: 'score',
|
|
59
|
-
targetScore: 10,
|
|
60
|
-
// No clock by default: a blank recipe that ended runs on a deadline nobody
|
|
61
|
-
// asked for would look like a bug in the mechanic being built.
|
|
62
|
-
timeLimitSec: 0,
|
|
63
|
-
startingLives: 3,
|
|
64
|
-
resultNode: 'result-overlay',
|
|
65
|
-
resultLabel: 'result-label',
|
|
66
|
-
winText: 'YOU WIN!',
|
|
67
|
-
loseText: 'GAME OVER',
|
|
68
|
-
retryButton: 'retry-button',
|
|
69
|
-
gameScene: 'res://scenes/main.pix3scene',
|
|
70
|
-
// Comma-separated node ids frozen on game over: every component on the node
|
|
71
|
-
// and on everything under it is disabled. Add your mechanic's nodes here as you build them — a spawner that
|
|
72
|
-
// keeps running behind the result screen is the classic leak.
|
|
73
|
-
freezeNodes: '',
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
static getPropertySchema(): PropertySchema {
|
|
78
|
-
const num = (name: string, label: string, min: number, max: number, step: number) => ({
|
|
79
|
-
name,
|
|
80
|
-
type: 'number' as const,
|
|
81
|
-
ui: { label, group: 'Rules', min, max, step, slider: true },
|
|
82
|
-
getValue: (s: unknown) => (s as GameRules).config[name],
|
|
83
|
-
setValue: (s: unknown, v: unknown) => {
|
|
84
|
-
const n = Number(v);
|
|
85
|
-
(s as GameRules).config[name] = Math.min(max, Math.max(min, Number.isFinite(n) ? n : min));
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
const str = (name: string, label: string, group: string) => ({
|
|
89
|
-
name,
|
|
90
|
-
type: 'string' as const,
|
|
91
|
-
ui: { label, group },
|
|
92
|
-
getValue: (s: unknown) => (s as GameRules).config[name],
|
|
93
|
-
setValue: (s: unknown, v: unknown) => {
|
|
94
|
-
(s as GameRules).config[name] = typeof v === 'string' ? v : '';
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
nodeType: 'GameRules',
|
|
100
|
-
properties: [
|
|
101
|
-
{
|
|
102
|
-
name: 'winMode',
|
|
103
|
-
type: 'select',
|
|
104
|
-
ui: { label: 'Win Mode', group: 'Rules', options: ['score', 'time', 'survive'] },
|
|
105
|
-
getValue: s => (s as GameRules).config.winMode,
|
|
106
|
-
setValue: (s, v) => {
|
|
107
|
-
(s as GameRules).config.winMode = v === 'time' || v === 'survive' ? v : 'score';
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
num('targetScore', 'Target Score', 1, 99999, 1),
|
|
111
|
-
num('timeLimitSec', 'Time Limit (s)', 0, 600, 1),
|
|
112
|
-
num('startingLives', 'Starting Lives', 1, 20, 1),
|
|
113
|
-
str('winText', 'Win Text', 'Result'),
|
|
114
|
-
str('loseText', 'Lose Text', 'Result'),
|
|
115
|
-
str('resultNode', 'Result Overlay', 'Result'),
|
|
116
|
-
str('resultLabel', 'Result Label', 'Result'),
|
|
117
|
-
str('retryButton', 'Retry Button', 'Flow'),
|
|
118
|
-
str('gameScene', 'Game Scene', 'Flow'),
|
|
119
|
-
str('freezeNodes', 'Freeze Nodes', 'Flow'),
|
|
120
|
-
],
|
|
121
|
-
groups: {
|
|
122
|
-
Rules: { label: 'Rules', expanded: true },
|
|
123
|
-
Result: { label: 'Result Screen', expanded: true },
|
|
124
|
-
Flow: { label: 'Flow', expanded: false },
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
onStart(): void {
|
|
130
|
-
this.lives = Math.max(1, Number(this.config.startingLives) || 1);
|
|
131
|
-
const dispose = this.scene?.commands.register('restart', () => this.restart(), {
|
|
132
|
-
description: 'Reload the game scene for a fresh run.',
|
|
133
|
-
});
|
|
134
|
-
this.disposeCommands = dispose ? [dispose] : [];
|
|
135
|
-
|
|
136
|
-
this.disposeDebug = registerGameDebug({
|
|
137
|
-
name: 'recipe-blank-2d',
|
|
138
|
-
version: 1,
|
|
139
|
-
// The registry IS the action list — never a second, hand-kept copy.
|
|
140
|
-
actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
|
|
141
|
-
reset: () => this.resetRun(),
|
|
142
|
-
snapshot: () => ({
|
|
143
|
-
scene: 'game',
|
|
144
|
-
phase: this.over ? (this.won ? 'won' : 'lost') : 'playing',
|
|
145
|
-
score: this.score,
|
|
146
|
-
lives: this.lives,
|
|
147
|
-
winMode: String(this.config.winMode ?? 'score'),
|
|
148
|
-
targetScore: Math.max(1, Number(this.config.targetScore) || 1),
|
|
149
|
-
elapsedSec: roundHundredths(this.elapsed),
|
|
150
|
-
// null when the run has no clock at all.
|
|
151
|
-
timeLeftSec: this.timeLeftSec(),
|
|
152
|
-
}),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const owner = this.node;
|
|
156
|
-
// The signal channel: a mechanic script can score without holding a reference
|
|
157
|
-
// to this component. Same effect as calling the methods below.
|
|
158
|
-
owner?.connect('score-added', this, (...args: unknown[]) =>
|
|
159
|
-
this.addScore(Number(args[0]) || 0)
|
|
160
|
-
);
|
|
161
|
-
owner?.connect('life-lost', this, (...args: unknown[]) => this.loseLife(Number(args[0]) || 1));
|
|
162
|
-
// The HUD announces itself once it is connected, so it never misses the
|
|
163
|
-
// opening values (its onStart runs after this one).
|
|
164
|
-
owner?.connect('hud-ready', this, () => this.broadcast());
|
|
165
|
-
|
|
166
|
-
this.setNodeVisible(String(this.config.resultNode ?? ''), false);
|
|
167
|
-
this.bindButton(
|
|
168
|
-
String(this.config.retryButton ?? ''),
|
|
169
|
-
() => this.scene?.commands.dispatch('restart'),
|
|
170
|
-
false
|
|
171
|
-
);
|
|
172
|
-
this.broadcast();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
onUpdate(dt: number): void {
|
|
176
|
-
if (this.over) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
this.elapsed += dt;
|
|
180
|
-
const limit = Math.max(0, Number(this.config.timeLimitSec) || 0);
|
|
181
|
-
this.node?.emit('time-changed', limit > 0 ? Math.max(0, limit - this.elapsed) : this.elapsed);
|
|
182
|
-
|
|
183
|
-
if (this.lives <= 0) {
|
|
184
|
-
this.finish(false);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const raw = String(this.config.winMode ?? 'score');
|
|
188
|
-
const mode: WinMode = raw === 'time' ? 'time' : raw === 'survive' ? 'survive' : 'score';
|
|
189
|
-
const target = Math.max(1, Number(this.config.targetScore) || 1);
|
|
190
|
-
const timeUp = limit > 0 && this.elapsed >= limit;
|
|
191
|
-
|
|
192
|
-
if (mode === 'score') {
|
|
193
|
-
if (this.score >= target) this.finish(true);
|
|
194
|
-
else if (timeUp) this.finish(false);
|
|
195
|
-
} else if (mode === 'time') {
|
|
196
|
-
if (timeUp) this.finish(this.score >= target);
|
|
197
|
-
} else if (timeUp) {
|
|
198
|
-
this.finish(true);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
onDetach(): void {
|
|
203
|
-
// Clearing is safe in any order: the disposer only clears the global when it
|
|
204
|
-
// still holds *this* provider, so a scene that already registered its own wins.
|
|
205
|
-
this.disposeDebug?.();
|
|
206
|
-
this.disposeDebug = null;
|
|
207
|
-
for (const dispose of this.disposeCommands) {
|
|
208
|
-
dispose();
|
|
209
|
-
}
|
|
210
|
-
this.disposeCommands = [];
|
|
211
|
-
super.onDetach();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Seconds left on the clock, or `null` when this run has no time limit. */
|
|
215
|
-
private timeLeftSec(): number | null {
|
|
216
|
-
const limit = Math.max(0, Number(this.config.timeLimitSec) || 0);
|
|
217
|
-
return limit > 0 ? roundHundredths(Math.max(0, limit - this.elapsed)) : null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Score. Call it from your mechanic, or emit `score-added` on this node. */
|
|
221
|
-
addScore(amount: number): void {
|
|
222
|
-
if (this.over) return;
|
|
223
|
-
this.score += amount;
|
|
224
|
-
this.node?.emit('score-changed', this.score);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Lose lives. Call it from your mechanic, or emit `life-lost` on this node. */
|
|
228
|
-
loseLife(amount = 1): void {
|
|
229
|
-
if (this.over) return;
|
|
230
|
-
this.lives = Math.max(0, this.lives - Math.max(1, amount));
|
|
231
|
-
this.node?.emit(
|
|
232
|
-
'lives-changed',
|
|
233
|
-
this.lives,
|
|
234
|
-
Math.max(1, Number(this.config.startingLives) || 1)
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private broadcast(): void {
|
|
239
|
-
const owner = this.node;
|
|
240
|
-
owner?.emit('score-changed', this.score);
|
|
241
|
-
owner?.emit('lives-changed', this.lives, Math.max(1, Number(this.config.startingLives) || 1));
|
|
242
|
-
const limit = Math.max(0, Number(this.config.timeLimitSec) || 0);
|
|
243
|
-
owner?.emit('time-changed', limit > 0 ? Math.max(0, limit - this.elapsed) : this.elapsed);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** End the run. This is the ONLY sanctioned way to show the result screen. */
|
|
247
|
-
finish(won: boolean): void {
|
|
248
|
-
if (this.over) return;
|
|
249
|
-
this.over = true;
|
|
250
|
-
this.won = won;
|
|
251
|
-
|
|
252
|
-
const label = this.findNode(String(this.config.resultLabel ?? ''));
|
|
253
|
-
if (label instanceof Label2D) {
|
|
254
|
-
label.setText(String((won ? this.config.winText : this.config.loseText) ?? ''));
|
|
255
|
-
}
|
|
256
|
-
this.setNodeVisible(String(this.config.resultNode ?? ''), true);
|
|
257
|
-
this.setButtonEnabled(String(this.config.retryButton ?? ''), true);
|
|
258
|
-
|
|
259
|
-
// A freeze node freezes its whole subtree: the spawners' own components AND
|
|
260
|
-
// every instance they spawned under themselves (a chaser's script, a pickup's
|
|
261
|
-
// pop-in), so nothing keeps moving over the result card.
|
|
262
|
-
for (const id of String(this.config.freezeNodes ?? '').split(',')) {
|
|
263
|
-
const root = this.findNode(id.trim());
|
|
264
|
-
if (!root) continue;
|
|
265
|
-
root.traverse(object => {
|
|
266
|
-
if (!(object instanceof NodeBase)) return;
|
|
267
|
-
for (const component of object.components) {
|
|
268
|
-
// Only remember what WE switched off: reviving everything on reset would
|
|
269
|
-
// silently enable a component the scene author had left disabled.
|
|
270
|
-
if (!component.enabled) continue;
|
|
271
|
-
component.enabled = false;
|
|
272
|
-
this.frozen.push(component);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
this.node?.emit(won ? 'game-won' : 'game-lost', this.score);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private restart(): void {
|
|
280
|
-
void this.scene?.changeScene(String(this.config.gameScene ?? ''), { transition: 'fade' });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Back to the state `onStart` left behind: score, lives, clock and outcome, the
|
|
285
|
-
* result overlay hidden, the freeze `finish()` applied lifted, and anything a
|
|
286
|
-
* `freezeNodes` component still has on the field dropped. Those leftovers are
|
|
287
|
-
* state too — a run that begins amid the previous run's objects did not begin
|
|
288
|
-
* from the start.
|
|
289
|
-
*
|
|
290
|
-
* Public because it is useful on its own (an in-run "try again" that skips the
|
|
291
|
-
* scene reload); the debug provider's `reset` is this method.
|
|
292
|
-
*/
|
|
293
|
-
resetRun(): void {
|
|
294
|
-
for (const component of this.frozen) {
|
|
295
|
-
component.enabled = true;
|
|
296
|
-
}
|
|
297
|
-
this.frozen = [];
|
|
298
|
-
this.clearSpawned();
|
|
299
|
-
|
|
300
|
-
this.score = 0;
|
|
301
|
-
this.lives = Math.max(1, Number(this.config.startingLives) || 1);
|
|
302
|
-
this.elapsed = 0;
|
|
303
|
-
this.over = false;
|
|
304
|
-
this.won = false;
|
|
305
|
-
|
|
306
|
-
this.setNodeVisible(String(this.config.resultNode ?? ''), false);
|
|
307
|
-
this.setButtonEnabled(String(this.config.retryButton ?? ''), false);
|
|
308
|
-
this.broadcast();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Ask every component on a `freezeNodes` node that owns spawned instances to
|
|
313
|
-
* drop them. Duck-typed on purpose: the freeze list is whatever you wrote there,
|
|
314
|
-
* so a spawner you write yourself joins in by offering a `clear()` method.
|
|
315
|
-
*/
|
|
316
|
-
private clearSpawned(): void {
|
|
317
|
-
for (const id of String(this.config.freezeNodes ?? '').split(',')) {
|
|
318
|
-
const node = this.findNode(id.trim());
|
|
319
|
-
if (!node) continue;
|
|
320
|
-
for (const component of node.components) {
|
|
321
|
-
const clear = (component as { clear?: unknown }).clear;
|
|
322
|
-
if (typeof clear === 'function') {
|
|
323
|
-
(clear as () => void).call(component);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private bindButton(query: string, handler: () => void, enabled: boolean): void {
|
|
330
|
-
const button = this.findNode(query);
|
|
331
|
-
if (button instanceof Button2D) {
|
|
332
|
-
button.enabled = enabled;
|
|
333
|
-
button.connect('pressed', this, handler);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private setButtonEnabled(query: string, enabled: boolean): void {
|
|
338
|
-
const button = this.findNode(query);
|
|
339
|
-
if (button instanceof Button2D) {
|
|
340
|
-
button.enabled = enabled;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
private setNodeVisible(query: string, visible: boolean): void {
|
|
345
|
-
const node = this.findNode(query);
|
|
346
|
-
if (node) {
|
|
347
|
-
node.visible = visible;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
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
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
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
|
-
```
|