@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,113 +1,113 @@
|
|
|
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
|
-
* Identical to the one the 2D blank recipe ships, down to the signal names: the HUD is
|
|
16
|
-
* 2D in both, because a 3D game's score is still read on a flat overlay.
|
|
17
|
-
*/
|
|
18
|
-
import { Bar2D, Label2D, Script, type PropertySchema } from '@pix3/runtime';
|
|
19
|
-
|
|
20
|
-
export class ScoreHud extends Script {
|
|
21
|
-
constructor(id: string, type: string) {
|
|
22
|
-
super(id, type);
|
|
23
|
-
this.config = {
|
|
24
|
-
// Node that emits the HUD signals (the GameRules host).
|
|
25
|
-
sourceNode: 'hud-root',
|
|
26
|
-
scoreLabel: 'score-label',
|
|
27
|
-
scoreFormat: 'SCORE {value}',
|
|
28
|
-
timeLabel: 'time-label',
|
|
29
|
-
timeFormat: '{value}',
|
|
30
|
-
livesBar: 'lives-bar',
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static getPropertySchema(): PropertySchema {
|
|
35
|
-
const str = (name: string, label: string, description?: string) => ({
|
|
36
|
-
name,
|
|
37
|
-
type: 'string' as const,
|
|
38
|
-
ui: { label, group: 'HUD', description },
|
|
39
|
-
getValue: (s: unknown) => (s as ScoreHud).config[name],
|
|
40
|
-
setValue: (s: unknown, v: unknown) => {
|
|
41
|
-
(s as ScoreHud).config[name] = typeof v === 'string' ? v : '';
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
nodeType: 'ScoreHud',
|
|
47
|
-
properties: [
|
|
48
|
-
str(
|
|
49
|
-
'sourceNode',
|
|
50
|
-
'Source Node',
|
|
51
|
-
'Node that emits score-changed / lives-changed / time-changed'
|
|
52
|
-
),
|
|
53
|
-
str('scoreLabel', 'Score Label'),
|
|
54
|
-
str('scoreFormat', 'Score Format', '{value} is replaced by the score'),
|
|
55
|
-
str('timeLabel', 'Time Label'),
|
|
56
|
-
str('timeFormat', 'Time Format', '{value} is replaced by whole seconds'),
|
|
57
|
-
str('livesBar', 'Lives Bar'),
|
|
58
|
-
],
|
|
59
|
-
groups: { HUD: { label: 'HUD', expanded: true } },
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
onStart(): void {
|
|
64
|
-
const source = this.findNode(String(this.config.sourceNode ?? ''));
|
|
65
|
-
if (!source) {
|
|
66
|
-
console.warn(`[ScoreHud] Source node "${this.config.sourceNode}" not found.`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
source.connect('score-changed', this, (...args: unknown[]) =>
|
|
70
|
-
this.setScore(Number(args[0]) || 0)
|
|
71
|
-
);
|
|
72
|
-
source.connect('time-changed', this, (...args: unknown[]) =>
|
|
73
|
-
this.setTime(Number(args[0]) || 0)
|
|
74
|
-
);
|
|
75
|
-
source.connect('lives-changed', this, (...args: unknown[]) =>
|
|
76
|
-
this.setLives(Number(args[0]) || 0, Number(args[1]) || 0)
|
|
77
|
-
);
|
|
78
|
-
// Ask for the opening values now that the listeners are live: GameRules
|
|
79
|
-
// starts before this HUD (parent components tick before children).
|
|
80
|
-
source.emit('hud-ready');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private setScore(score: number): void {
|
|
84
|
-
const label = this.findNode(String(this.config.scoreLabel ?? ''));
|
|
85
|
-
if (label instanceof Label2D) {
|
|
86
|
-
label.setText(
|
|
87
|
-
String(this.config.scoreFormat ?? '{value}').replace('{value}', String(Math.round(score)))
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private setTime(seconds: number): void {
|
|
93
|
-
const label = this.findNode(String(this.config.timeLabel ?? ''));
|
|
94
|
-
if (label instanceof Label2D) {
|
|
95
|
-
label.setText(
|
|
96
|
-
String(this.config.timeFormat ?? '{value}').replace(
|
|
97
|
-
'{value}',
|
|
98
|
-
String(Math.max(0, Math.ceil(seconds)))
|
|
99
|
-
)
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private setLives(lives: number, maxLives: number): void {
|
|
105
|
-
const bar = this.findNode(String(this.config.livesBar ?? ''));
|
|
106
|
-
if (bar instanceof Bar2D) {
|
|
107
|
-
if (maxLives > 0) {
|
|
108
|
-
bar.maxValue = maxLives;
|
|
109
|
-
}
|
|
110
|
-
bar.setValue(lives);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
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
|
+
* Identical to the one the 2D blank recipe ships, down to the signal names: the HUD is
|
|
16
|
+
* 2D in both, because a 3D game's score is still read on a flat overlay.
|
|
17
|
+
*/
|
|
18
|
+
import { Bar2D, Label2D, Script, type PropertySchema } from '@pix3/runtime';
|
|
19
|
+
|
|
20
|
+
export class ScoreHud extends Script {
|
|
21
|
+
constructor(id: string, type: string) {
|
|
22
|
+
super(id, type);
|
|
23
|
+
this.config = {
|
|
24
|
+
// Node that emits the HUD signals (the GameRules host).
|
|
25
|
+
sourceNode: 'hud-root',
|
|
26
|
+
scoreLabel: 'score-label',
|
|
27
|
+
scoreFormat: 'SCORE {value}',
|
|
28
|
+
timeLabel: 'time-label',
|
|
29
|
+
timeFormat: '{value}',
|
|
30
|
+
livesBar: 'lives-bar',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static getPropertySchema(): PropertySchema {
|
|
35
|
+
const str = (name: string, label: string, description?: string) => ({
|
|
36
|
+
name,
|
|
37
|
+
type: 'string' as const,
|
|
38
|
+
ui: { label, group: 'HUD', description },
|
|
39
|
+
getValue: (s: unknown) => (s as ScoreHud).config[name],
|
|
40
|
+
setValue: (s: unknown, v: unknown) => {
|
|
41
|
+
(s as ScoreHud).config[name] = typeof v === 'string' ? v : '';
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
nodeType: 'ScoreHud',
|
|
47
|
+
properties: [
|
|
48
|
+
str(
|
|
49
|
+
'sourceNode',
|
|
50
|
+
'Source Node',
|
|
51
|
+
'Node that emits score-changed / lives-changed / time-changed'
|
|
52
|
+
),
|
|
53
|
+
str('scoreLabel', 'Score Label'),
|
|
54
|
+
str('scoreFormat', 'Score Format', '{value} is replaced by the score'),
|
|
55
|
+
str('timeLabel', 'Time Label'),
|
|
56
|
+
str('timeFormat', 'Time Format', '{value} is replaced by whole seconds'),
|
|
57
|
+
str('livesBar', 'Lives Bar'),
|
|
58
|
+
],
|
|
59
|
+
groups: { HUD: { label: 'HUD', expanded: true } },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onStart(): void {
|
|
64
|
+
const source = this.findNode(String(this.config.sourceNode ?? ''));
|
|
65
|
+
if (!source) {
|
|
66
|
+
console.warn(`[ScoreHud] Source node "${this.config.sourceNode}" not found.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
source.connect('score-changed', this, (...args: unknown[]) =>
|
|
70
|
+
this.setScore(Number(args[0]) || 0)
|
|
71
|
+
);
|
|
72
|
+
source.connect('time-changed', this, (...args: unknown[]) =>
|
|
73
|
+
this.setTime(Number(args[0]) || 0)
|
|
74
|
+
);
|
|
75
|
+
source.connect('lives-changed', this, (...args: unknown[]) =>
|
|
76
|
+
this.setLives(Number(args[0]) || 0, Number(args[1]) || 0)
|
|
77
|
+
);
|
|
78
|
+
// Ask for the opening values now that the listeners are live: GameRules
|
|
79
|
+
// starts before this HUD (parent components tick before children).
|
|
80
|
+
source.emit('hud-ready');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private setScore(score: number): void {
|
|
84
|
+
const label = this.findNode(String(this.config.scoreLabel ?? ''));
|
|
85
|
+
if (label instanceof Label2D) {
|
|
86
|
+
label.setText(
|
|
87
|
+
String(this.config.scoreFormat ?? '{value}').replace('{value}', String(Math.round(score)))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private setTime(seconds: number): void {
|
|
93
|
+
const label = this.findNode(String(this.config.timeLabel ?? ''));
|
|
94
|
+
if (label instanceof Label2D) {
|
|
95
|
+
label.setText(
|
|
96
|
+
String(this.config.timeFormat ?? '{value}').replace(
|
|
97
|
+
'{value}',
|
|
98
|
+
String(Math.max(0, Math.ceil(seconds)))
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private setLives(lives: number, maxLives: number): void {
|
|
105
|
+
const bar = this.findNode(String(this.config.livesBar ?? ''));
|
|
106
|
+
if (bar instanceof Bar2D) {
|
|
107
|
+
if (maxLives > 0) {
|
|
108
|
+
bar.maxValue = maxLives;
|
|
109
|
+
}
|
|
110
|
+
bar.setValue(lives);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
id: playable-3d
|
|
2
|
-
title: Playable 3D
|
|
3
|
-
description: Portrait 3D playable-ad blank — tap-to-start intro (unlocks audio), gameplay placeholder, end screen with a store CTA button.
|
|
4
|
-
# Flow recipe this template serves (see design/recipe.md).
|
|
5
|
-
recipeId: recipe-scene-3d
|
|
6
|
-
projectType: 3d
|
|
7
|
-
targetPlatform: mobile
|
|
8
|
-
viewport: { width: 1080, height: 1920 }
|
|
9
|
-
order: 30
|
|
1
|
+
id: playable-3d
|
|
2
|
+
title: Playable 3D
|
|
3
|
+
description: Portrait 3D playable-ad blank — tap-to-start intro (unlocks audio), gameplay placeholder, end screen with a store CTA button.
|
|
4
|
+
# Flow recipe this template serves (see design/recipe.md).
|
|
5
|
+
recipeId: recipe-scene-3d
|
|
6
|
+
projectType: 3d
|
|
7
|
+
targetPlatform: mobile
|
|
8
|
+
viewport: { width: 1080, height: 1920 }
|
|
9
|
+
order: 30
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
# Arena 2D
|
|
2
|
-
|
|
3
|
-
A playable skeleton: an avatar in a bounded field, two spawners dripping
|
|
4
|
-
pickups and hazards, touch resolved into score or damage, lives + a timer, and a
|
|
5
|
-
win/lose screen with RETRY / MENU. It plays as shipped — press play in
|
|
6
|
-
`scenes/main.pix3scene` before changing anything.
|
|
7
|
-
|
|
8
|
-
**`design/recipe.md` is the contract.** It lists the stable node ids, the
|
|
9
|
-
tunables (with their ranges), the extension points and what must not be renamed.
|
|
10
|
-
Read it first; tools and agents grep it.
|
|
11
|
-
|
|
12
|
-
## Layout
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
scenes/menu.pix3scene entry / export scene — PLAY transitions to the game
|
|
16
|
-
scenes/main.pix3scene the GAME (editor startup scene — iterate here)
|
|
17
|
-
scenes/ui/result.pix3scene win/lose overlay — instanced into main hidden
|
|
18
|
-
scenes/prefabs/*.pix3scene what the spawners instantiate
|
|
19
|
-
scripts/PlayerController.ts how the avatar moves (swap this file whole)
|
|
20
|
-
scripts/Spawner.ts timed instantiate + drift + despawn
|
|
21
|
-
scripts/TouchRules.ts contact → semantic signals
|
|
22
|
-
scripts/ScoreHud.ts signals → HUD widgets
|
|
23
|
-
scripts/GameRules.ts score / lives / timer / win / lose / end flow
|
|
24
|
-
scripts/MenuFlow.ts menu PLAY button
|
|
25
|
-
sprites/ph-*.png near-white placeholders (tinted via core:tint)
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Full-screen UI lives in its own scene file, never inline in the game scene.
|
|
29
|
-
`main.pix3scene` carries the end screen as a one-line `instance:` marked
|
|
30
|
-
`visible: false` — that hides it in the **editor** only, so opening the project
|
|
31
|
-
shows the game instead of a GAME OVER card. What play mode reads is
|
|
32
|
-
`initiallyVisible`, authored in `scenes/ui/result.pix3scene`. Edit the overlay
|
|
33
|
-
full-screen in its own tab (right-click → Open Prefab); tick the eye in
|
|
34
|
-
`main.pix3scene` to preview it composited, and untick it before saving.
|
|
35
|
-
|
|
36
|
-
## The rule that keeps it workable
|
|
37
|
-
|
|
38
|
-
One file, one responsibility. Each script is 70–140 lines on purpose: small
|
|
39
|
-
files can be edited surgically; monoliths get rewritten wholesale and lose the
|
|
40
|
-
changes that were already working. When you add a mechanic, add a script.
|
|
41
|
-
|
|
42
|
-
Signals are the seam between them:
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
TouchRules --touch-scored/touch-damaged--> GameRules --score-changed--> ScoreHud
|
|
46
|
-
--lives-changed-->
|
|
47
|
-
--time-changed-->
|
|
48
|
-
```
|
|
1
|
+
# Arena 2D
|
|
2
|
+
|
|
3
|
+
A playable skeleton: an avatar in a bounded field, two spawners dripping
|
|
4
|
+
pickups and hazards, touch resolved into score or damage, lives + a timer, and a
|
|
5
|
+
win/lose screen with RETRY / MENU. It plays as shipped — press play in
|
|
6
|
+
`scenes/main.pix3scene` before changing anything.
|
|
7
|
+
|
|
8
|
+
**`design/recipe.md` is the contract.** It lists the stable node ids, the
|
|
9
|
+
tunables (with their ranges), the extension points and what must not be renamed.
|
|
10
|
+
Read it first; tools and agents grep it.
|
|
11
|
+
|
|
12
|
+
## Layout
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
scenes/menu.pix3scene entry / export scene — PLAY transitions to the game
|
|
16
|
+
scenes/main.pix3scene the GAME (editor startup scene — iterate here)
|
|
17
|
+
scenes/ui/result.pix3scene win/lose overlay — instanced into main hidden
|
|
18
|
+
scenes/prefabs/*.pix3scene what the spawners instantiate
|
|
19
|
+
scripts/PlayerController.ts how the avatar moves (swap this file whole)
|
|
20
|
+
scripts/Spawner.ts timed instantiate + drift + despawn
|
|
21
|
+
scripts/TouchRules.ts contact → semantic signals
|
|
22
|
+
scripts/ScoreHud.ts signals → HUD widgets
|
|
23
|
+
scripts/GameRules.ts score / lives / timer / win / lose / end flow
|
|
24
|
+
scripts/MenuFlow.ts menu PLAY button
|
|
25
|
+
sprites/ph-*.png near-white placeholders (tinted via core:tint)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Full-screen UI lives in its own scene file, never inline in the game scene.
|
|
29
|
+
`main.pix3scene` carries the end screen as a one-line `instance:` marked
|
|
30
|
+
`visible: false` — that hides it in the **editor** only, so opening the project
|
|
31
|
+
shows the game instead of a GAME OVER card. What play mode reads is
|
|
32
|
+
`initiallyVisible`, authored in `scenes/ui/result.pix3scene`. Edit the overlay
|
|
33
|
+
full-screen in its own tab (right-click → Open Prefab); tick the eye in
|
|
34
|
+
`main.pix3scene` to preview it composited, and untick it before saving.
|
|
35
|
+
|
|
36
|
+
## The rule that keeps it workable
|
|
37
|
+
|
|
38
|
+
One file, one responsibility. Each script is 70–140 lines on purpose: small
|
|
39
|
+
files can be edited surgically; monoliths get rewritten wholesale and lose the
|
|
40
|
+
changes that were already working. When you add a mechanic, add a script.
|
|
41
|
+
|
|
42
|
+
Signals are the seam between them:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
TouchRules --touch-scored/touch-damaged--> GameRules --score-changed--> ScoreHud
|
|
46
|
+
--lives-changed-->
|
|
47
|
+
--time-changed-->
|
|
48
|
+
```
|
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
# Recipe: recipe-arena-2d
|
|
2
|
-
|
|
3
|
-
## What this is
|
|
4
|
-
|
|
5
|
-
An avatar in a bounded field; two spawners drip things in; touching a thing
|
|
6
|
-
resolves through a group→outcome map into score or damage. Lives, timer,
|
|
7
|
-
win/lose screen with RETRY / MENU. Playable before any edit — start from what
|
|
8
|
-
runs. This is the **fallback recipe**: anything shaped like "steer something
|
|
9
|
-
around a space while stuff comes at you" starts here.
|
|
10
|
-
|
|
11
|
-
## Node map
|
|
12
|
-
|
|
13
|
-
| id | what |
|
|
14
|
-
| --- | --- |
|
|
15
|
-
| `game-root` | root; hosts `GameRules` + `TouchRules`; all HUD signals are emitted here |
|
|
16
|
-
| `game-background` | full-screen `ColorRect2D` (palette background) |
|
|
17
|
-
| `board` | the field (`Group2D`); its rect clamps the avatar |
|
|
18
|
-
| `board-floor` | the field's visible plate |
|
|
19
|
-
| `player` | avatar (`Sprite2D`), hosts `PlayerController` |
|
|
20
|
-
| `spawner-pickups` / `spawner-hazards` | spawn bands, one `Spawner` each |
|
|
21
|
-
| `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
|
|
22
|
-
| `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
|
|
23
|
-
| `menu-button` | back to the menu scene |
|
|
24
|
-
| `result-overlay` / `result-label` / `retry-button` | end screen — authored in `scenes/ui/result.pix3scene`, instanced into `hud` with `visible: false` so the editor opens on the game; `initiallyVisible` and `GameRules` own it at run time |
|
|
25
|
-
|
|
26
|
-
Signals: `TouchRules` emits `touch-scored` / `touch-damaged` on `game-root`;
|
|
27
|
-
`GameRules` answers `score-changed`, `lives-changed`, `time-changed`,
|
|
28
|
-
`game-won` / `game-lost`.
|
|
29
|
-
|
|
30
|
-
## Placeholders
|
|
31
|
-
|
|
32
|
-
Near-white PNGs carrying a `core:tint` effect: set `effects[0].params.color` and
|
|
33
|
-
the multiply gives you that palette colour.
|
|
34
|
-
|
|
35
|
-
| role | file | node/prefab |
|
|
36
|
-
| --- | --- | --- |
|
|
37
|
-
| avatar | `sprites/ph-avatar.png` | `player` |
|
|
38
|
-
| collectible | `sprites/ph-pickup.png` | `prefabs/pickup.pix3scene` |
|
|
39
|
-
| threat | `sprites/ph-hazard.png` | `prefabs/hazard.pix3scene` |
|
|
40
|
-
|
|
41
|
-
## Tunables
|
|
42
|
-
|
|
43
|
-
```yaml
|
|
44
|
-
tunables:
|
|
45
|
-
playerMode: { node: player, component: "user:PlayerController", property: mode, default: pointer }
|
|
46
|
-
playerSpeed: { node: player, component: "user:PlayerController", property: speed, min: 60, max: 2000, default: 620 }
|
|
47
|
-
playerRadius: { node: player, component: "user:PlayerController", property: radius, min: 4, max: 400, default: 60 }
|
|
48
|
-
pickupRate: { node: spawner-pickups, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 1.0 }
|
|
49
|
-
pickupMaxAlive: { node: spawner-pickups, component: "user:Spawner", property: maxAlive, min: 1, max: 200, default: 10 }
|
|
50
|
-
pickupFallSpeed: { node: spawner-pickups, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -300 }
|
|
51
|
-
hazardRate: { node: spawner-hazards, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 1.8 }
|
|
52
|
-
hazardFallSpeed: { node: spawner-hazards, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -380 }
|
|
53
|
-
touchRadius: { node: game-root, component: "user:TouchRules", property: touchRadius, min: 4, max: 600, default: 62 }
|
|
54
|
-
touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "pickup:score:1;hazard:damage:1" }
|
|
55
|
-
winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
|
|
56
|
-
targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 12 }
|
|
57
|
-
timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 45 }
|
|
58
|
-
startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
|
|
59
|
-
bgColor: { node: game-background, property: color, default: "#141a2e" }
|
|
60
|
-
boardColor: { node: board-floor, property: color, default: "#1d2743" }
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
`component` present → `set_component_property`; absent → `set_property`. The
|
|
64
|
-
schema clamps out-of-range values rather than rejecting them.
|
|
65
|
-
|
|
66
|
-
## Extension points
|
|
67
|
-
|
|
68
|
-
- **Different locomotion → replace `scripts/PlayerController.ts` wholesale.** It
|
|
69
|
-
is the only file that reads input and writes `player.position`. A grid stepper,
|
|
70
|
-
a jumper (velocity + gravity) or an auto-runner all drop in as long as the file
|
|
71
|
-
still exports a `PlayerController` class and keeps the node inside `board`.
|
|
72
|
-
Write it in ONE call — `fs_write` with `overwrite: true` and a `reason` (the
|
|
73
|
-
size guard refuses a silent wholesale rewrite; this one is sanctioned).
|
|
74
|
-
- **Trail / body behind the avatar.** Add a `segment` prefab (`Sprite2D` +
|
|
75
|
-
`core:Hitbox2D` in a hazard group). In the locomotion script, on each completed
|
|
76
|
-
step `scene.instantiate` a segment at the vacated position into a `segments`
|
|
77
|
-
node and `queueFree` the tail once the trail exceeds its length. Self-collision
|
|
78
|
-
then falls out of the existing rules — the segment's group is already in
|
|
79
|
-
`touchRules`, so hitting your own trail costs a life with no new code. Grow the
|
|
80
|
-
trail by skipping the tail free on a `touch-scored` frame.
|
|
81
|
-
- **A third kind of thing.** Copy a prefab, give its `core:Hitbox2D` a new group,
|
|
82
|
-
`create_node` + `add_component` a third `Spawner`, append `<group>:score:5` to
|
|
83
|
-
`touchRules`. Chasers: a script on the prefab that steers toward `player`'s
|
|
84
|
-
world position — contact is already punished. Spawn it under a spawner (the
|
|
85
|
-
default `parentNode`) listed in `freezeNodes`: `finish()` freezes that whole
|
|
86
|
-
subtree, so the chaser stops on game over. Parented elsewhere, it keeps
|
|
87
|
-
crawling over the result card — list that parent in `freezeNodes` too.
|
|
88
|
-
- **More juice.** `scene.juice.punchScale/shake/flash` + `scene.time.hitstop` are
|
|
89
|
-
already called in `TouchRules`; raise the numbers there.
|
|
90
|
-
|
|
91
|
-
## Ending a run belongs to GameRules
|
|
92
|
-
|
|
93
|
-
The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
|
|
94
|
-
start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
|
|
95
|
-
writes the result text, enables RETRY and freezes the `freezeNodes` subtrees
|
|
96
|
-
(every component on them and on what they spawned). For a custom
|
|
97
|
-
win/lose condition call `finish(true|false)` on that component — do NOT show the
|
|
98
|
-
overlay yourself. A hand-rolled ending leaves RETRY on screen with its handler bound
|
|
99
|
-
and `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
|
|
100
|
-
`control: { enabled: false }`; that is the tell).
|
|
101
|
-
|
|
102
|
-
## Do not touch
|
|
103
|
-
|
|
104
|
-
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
105
|
-
- `visible: false` on the `result-overlay` instance in `main.pix3scene`. Tick the
|
|
106
|
-
eye to preview the end screen over the field, but never save it on: it is what
|
|
107
|
-
keeps every iteration from starting behind a GAME OVER card. Run-time
|
|
108
|
-
visibility is `initiallyVisible` in `scenes/ui/result.pix3scene`, not this.
|
|
109
|
-
- Component config on a **prefab instance** — the editor locks it. Prefabs carry
|
|
110
|
-
fixed defaults only (their hitbox group); tunables live on plain nodes in
|
|
111
|
-
`main.pix3scene`.
|
|
112
|
-
- Do not import rapier: there is no rigidbody solver here and none is needed.
|
|
113
|
-
|
|
114
|
-
## Playtest contract
|
|
115
|
-
|
|
116
|
-
terminalVisual: static
|
|
117
|
-
terminalRestartRoutine: terminal-retry
|
|
118
|
-
|
|
119
|
-
## Verify
|
|
120
|
-
|
|
121
|
-
1. `play_start` on `scenes/main.pix3scene`.
|
|
122
|
-
2. `game_input` a pointer drag across the lower board — `player`'s position
|
|
123
|
-
changes between two `game_observe` calls.
|
|
124
|
-
3. After ~3 s: children exist under `spawner-pickups`, and `score-label` climbs
|
|
125
|
-
above `SCORE 0` once the avatar meets one.
|
|
126
|
-
4. Set `targetScore: 1`, replay, collect one → `result-overlay` visible with
|
|
127
|
-
`YOU WIN!`; `retry-button` restarts the scene.
|
|
1
|
+
# Recipe: recipe-arena-2d
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
An avatar in a bounded field; two spawners drip things in; touching a thing
|
|
6
|
+
resolves through a group→outcome map into score or damage. Lives, timer,
|
|
7
|
+
win/lose screen with RETRY / MENU. Playable before any edit — start from what
|
|
8
|
+
runs. This is the **fallback recipe**: anything shaped like "steer something
|
|
9
|
+
around a space while stuff comes at you" starts here.
|
|
10
|
+
|
|
11
|
+
## Node map
|
|
12
|
+
|
|
13
|
+
| id | what |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `game-root` | root; hosts `GameRules` + `TouchRules`; all HUD signals are emitted here |
|
|
16
|
+
| `game-background` | full-screen `ColorRect2D` (palette background) |
|
|
17
|
+
| `board` | the field (`Group2D`); its rect clamps the avatar |
|
|
18
|
+
| `board-floor` | the field's visible plate |
|
|
19
|
+
| `player` | avatar (`Sprite2D`), hosts `PlayerController` |
|
|
20
|
+
| `spawner-pickups` / `spawner-hazards` | spawn bands, one `Spawner` each |
|
|
21
|
+
| `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
|
|
22
|
+
| `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
|
|
23
|
+
| `menu-button` | back to the menu scene |
|
|
24
|
+
| `result-overlay` / `result-label` / `retry-button` | end screen — authored in `scenes/ui/result.pix3scene`, instanced into `hud` with `visible: false` so the editor opens on the game; `initiallyVisible` and `GameRules` own it at run time |
|
|
25
|
+
|
|
26
|
+
Signals: `TouchRules` emits `touch-scored` / `touch-damaged` on `game-root`;
|
|
27
|
+
`GameRules` answers `score-changed`, `lives-changed`, `time-changed`,
|
|
28
|
+
`game-won` / `game-lost`.
|
|
29
|
+
|
|
30
|
+
## Placeholders
|
|
31
|
+
|
|
32
|
+
Near-white PNGs carrying a `core:tint` effect: set `effects[0].params.color` and
|
|
33
|
+
the multiply gives you that palette colour.
|
|
34
|
+
|
|
35
|
+
| role | file | node/prefab |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| avatar | `sprites/ph-avatar.png` | `player` |
|
|
38
|
+
| collectible | `sprites/ph-pickup.png` | `prefabs/pickup.pix3scene` |
|
|
39
|
+
| threat | `sprites/ph-hazard.png` | `prefabs/hazard.pix3scene` |
|
|
40
|
+
|
|
41
|
+
## Tunables
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
tunables:
|
|
45
|
+
playerMode: { node: player, component: "user:PlayerController", property: mode, default: pointer }
|
|
46
|
+
playerSpeed: { node: player, component: "user:PlayerController", property: speed, min: 60, max: 2000, default: 620 }
|
|
47
|
+
playerRadius: { node: player, component: "user:PlayerController", property: radius, min: 4, max: 400, default: 60 }
|
|
48
|
+
pickupRate: { node: spawner-pickups, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 1.0 }
|
|
49
|
+
pickupMaxAlive: { node: spawner-pickups, component: "user:Spawner", property: maxAlive, min: 1, max: 200, default: 10 }
|
|
50
|
+
pickupFallSpeed: { node: spawner-pickups, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -300 }
|
|
51
|
+
hazardRate: { node: spawner-hazards, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 1.8 }
|
|
52
|
+
hazardFallSpeed: { node: spawner-hazards, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -380 }
|
|
53
|
+
touchRadius: { node: game-root, component: "user:TouchRules", property: touchRadius, min: 4, max: 600, default: 62 }
|
|
54
|
+
touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "pickup:score:1;hazard:damage:1" }
|
|
55
|
+
winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
|
|
56
|
+
targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 12 }
|
|
57
|
+
timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 45 }
|
|
58
|
+
startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
|
|
59
|
+
bgColor: { node: game-background, property: color, default: "#141a2e" }
|
|
60
|
+
boardColor: { node: board-floor, property: color, default: "#1d2743" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`component` present → `set_component_property`; absent → `set_property`. The
|
|
64
|
+
schema clamps out-of-range values rather than rejecting them.
|
|
65
|
+
|
|
66
|
+
## Extension points
|
|
67
|
+
|
|
68
|
+
- **Different locomotion → replace `scripts/PlayerController.ts` wholesale.** It
|
|
69
|
+
is the only file that reads input and writes `player.position`. A grid stepper,
|
|
70
|
+
a jumper (velocity + gravity) or an auto-runner all drop in as long as the file
|
|
71
|
+
still exports a `PlayerController` class and keeps the node inside `board`.
|
|
72
|
+
Write it in ONE call — `fs_write` with `overwrite: true` and a `reason` (the
|
|
73
|
+
size guard refuses a silent wholesale rewrite; this one is sanctioned).
|
|
74
|
+
- **Trail / body behind the avatar.** Add a `segment` prefab (`Sprite2D` +
|
|
75
|
+
`core:Hitbox2D` in a hazard group). In the locomotion script, on each completed
|
|
76
|
+
step `scene.instantiate` a segment at the vacated position into a `segments`
|
|
77
|
+
node and `queueFree` the tail once the trail exceeds its length. Self-collision
|
|
78
|
+
then falls out of the existing rules — the segment's group is already in
|
|
79
|
+
`touchRules`, so hitting your own trail costs a life with no new code. Grow the
|
|
80
|
+
trail by skipping the tail free on a `touch-scored` frame.
|
|
81
|
+
- **A third kind of thing.** Copy a prefab, give its `core:Hitbox2D` a new group,
|
|
82
|
+
`create_node` + `add_component` a third `Spawner`, append `<group>:score:5` to
|
|
83
|
+
`touchRules`. Chasers: a script on the prefab that steers toward `player`'s
|
|
84
|
+
world position — contact is already punished. Spawn it under a spawner (the
|
|
85
|
+
default `parentNode`) listed in `freezeNodes`: `finish()` freezes that whole
|
|
86
|
+
subtree, so the chaser stops on game over. Parented elsewhere, it keeps
|
|
87
|
+
crawling over the result card — list that parent in `freezeNodes` too.
|
|
88
|
+
- **More juice.** `scene.juice.punchScale/shake/flash` + `scene.time.hitstop` are
|
|
89
|
+
already called in `TouchRules`; raise the numbers there.
|
|
90
|
+
|
|
91
|
+
## Ending a run belongs to GameRules
|
|
92
|
+
|
|
93
|
+
The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
|
|
94
|
+
start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
|
|
95
|
+
writes the result text, enables RETRY and freezes the `freezeNodes` subtrees
|
|
96
|
+
(every component on them and on what they spawned). For a custom
|
|
97
|
+
win/lose condition call `finish(true|false)` on that component — do NOT show the
|
|
98
|
+
overlay yourself. A hand-rolled ending leaves RETRY on screen with its handler bound
|
|
99
|
+
and `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
|
|
100
|
+
`control: { enabled: false }`; that is the tell).
|
|
101
|
+
|
|
102
|
+
## Do not touch
|
|
103
|
+
|
|
104
|
+
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
105
|
+
- `visible: false` on the `result-overlay` instance in `main.pix3scene`. Tick the
|
|
106
|
+
eye to preview the end screen over the field, but never save it on: it is what
|
|
107
|
+
keeps every iteration from starting behind a GAME OVER card. Run-time
|
|
108
|
+
visibility is `initiallyVisible` in `scenes/ui/result.pix3scene`, not this.
|
|
109
|
+
- Component config on a **prefab instance** — the editor locks it. Prefabs carry
|
|
110
|
+
fixed defaults only (their hitbox group); tunables live on plain nodes in
|
|
111
|
+
`main.pix3scene`.
|
|
112
|
+
- Do not import rapier: there is no rigidbody solver here and none is needed.
|
|
113
|
+
|
|
114
|
+
## Playtest contract
|
|
115
|
+
|
|
116
|
+
terminalVisual: static
|
|
117
|
+
terminalRestartRoutine: terminal-retry
|
|
118
|
+
|
|
119
|
+
## Verify
|
|
120
|
+
|
|
121
|
+
1. `play_start` on `scenes/main.pix3scene`.
|
|
122
|
+
2. `game_input` a pointer drag across the lower board — `player`'s position
|
|
123
|
+
changes between two `game_observe` calls.
|
|
124
|
+
3. After ~3 s: children exist under `spawner-pickups`, and `score-label` climbs
|
|
125
|
+
above `SCORE 0` once the avatar meets one.
|
|
126
|
+
4. Set `targetScore: 1`, replay, collect one → `result-overlay` visible with
|
|
127
|
+
`YOU WIN!`; `retry-button` restarts the scene.
|