@pix3/cli 1.6.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 -0
- package/README.md +754 -0
- package/dist/index.js +494 -0
- package/dist/runtime-types.json +1 -0
- package/dist/smoke/prebuilt/build-7MIXIMSZ.mjs +948 -0
- package/dist/smoke/prebuilt/chunk-J2SJVMDO.mjs +2 -0
- package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +239 -0
- package/dist/smoke/prebuilt/chunk-P2CBRXPJ.mjs +4050 -0
- package/dist/smoke/prebuilt/runtime.mjs +2 -0
- package/dist/smoke/prebuilt/smoke-worker.mjs +3 -0
- package/dist/smoke/prebuilt/three.mjs +2 -0
- package/dist/smoke/prebuilt/tree-defaults.mjs +2 -0
- package/dist/validate/prebuilt/build-AUGKXER3.js +948 -0
- package/dist/validate/prebuilt/bundle-main.js +24 -0
- package/dist/validate/prebuilt/chunk-6ACT6FNV.js +4050 -0
- package/dist/validate/prebuilt/chunk-BS2OZM7U.js +239 -0
- package/dist/validate/prebuilt/runtime.js +2 -0
- package/dist/validate/prebuilt/three.js +2 -0
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -0
- package/kit/files/.claude/skills/pix3-nodes/reference.md +1331 -0
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +235 -0
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +176 -0
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +281 -0
- package/kit/files/.claude/skills/pix3-scripts/reference.md +638 -0
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +386 -0
- package/kit/files/.claude/skills/pix3-verify/reference.md +21 -0
- package/kit/files/AGENTS.md +203 -0
- package/kit/files/CLAUDE.md +1 -0
- package/kit/kit.json +24 -0
- package/package.json +54 -0
- package/templates/empty-2d/files/README.md +20 -0
- package/templates/empty-2d/files/scenes/main.pix3scene +68 -0
- package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-2d/template.yaml +7 -0
- package/templates/empty-3d/files/README.md +20 -0
- package/templates/empty-3d/files/scenes/main.pix3scene +108 -0
- package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-3d/template.yaml +7 -0
- package/templates/idea-blank/files/README.md +22 -0
- package/templates/idea-blank/files/scenes/main.pix3scene +37 -0
- package/templates/idea-blank/template.yaml +10 -0
- package/templates/minigame-2d/files/README.md +44 -0
- package/templates/minigame-2d/files/design/tests/reachability.json +1 -0
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -0
- package/templates/minigame-2d/files/scenes/main.pix3scene +84 -0
- package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -0
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -0
- package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -0
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -0
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -0
- package/templates/minigame-2d/template.yaml +10 -0
- package/templates/playable-2d/files/README.md +30 -0
- package/templates/playable-2d/files/design/recipe.md +83 -0
- package/templates/playable-2d/files/design/tests/reachability.json +4 -0
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -0
- package/templates/playable-2d/files/scenes/main.pix3scene +111 -0
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -0
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -0
- package/templates/playable-2d/files/scripts/CtaButton.ts +55 -0
- package/templates/playable-2d/files/scripts/GameFlow.ts +256 -0
- package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/playable-2d/template.yaml +9 -0
- package/templates/playable-3d/files/README.md +32 -0
- package/templates/playable-3d/files/design/recipe.md +127 -0
- package/templates/playable-3d/files/design/tests/reachability.json +4 -0
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -0
- package/templates/playable-3d/files/scenes/main.pix3scene +230 -0
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -0
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -0
- package/templates/playable-3d/files/scripts/CtaButton.ts +55 -0
- package/templates/playable-3d/files/scripts/GameFlow.ts +309 -0
- package/templates/playable-3d/files/scripts/GameRules.ts +290 -0
- package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -0
- package/templates/playable-3d/template.yaml +9 -0
- package/templates/recipe-arena-2d/files/README.md +48 -0
- package/templates/recipe-arena-2d/files/design/recipe.md +127 -0
- package/templates/recipe-arena-2d/files/design/tests/reachability.json +4 -0
- package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +23 -0
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -0
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -0
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -0
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -0
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -0
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -0
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -0
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -0
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -0
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -0
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -0
- 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 +10 -0
- package/templates/recipe-blank-2d/files/README.md +61 -0
- package/templates/recipe-blank-2d/files/design/recipe.md +148 -0
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -0
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -0
- package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +23 -0
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -0
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -0
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -0
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -0
- 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 +10 -0
- package/templates/recipe-bouncer-2d/files/README.md +81 -0
- package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -0
- package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +4 -0
- package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +23 -0
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -0
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -0
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -0
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -0
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -0
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -0
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -0
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -0
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -0
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -0
- 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 +10 -0
- package/templates/recipe-grid-3d/files/README.md +22 -0
- package/templates/recipe-grid-3d/files/design/recipe.md +110 -0
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -0
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -0
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -0
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -0
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -0
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -0
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -0
- package/templates/recipe-grid-3d/template.yaml +10 -0
- package/templates/recipe-tapper-2d/files/README.md +39 -0
- package/templates/recipe-tapper-2d/files/design/recipe.md +132 -0
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -0
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -0
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -0
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -0
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -0
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -0
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -0
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -0
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -0
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -0
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -0
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -0
- 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 +10 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TouchRules — turns contact into meaning.
|
|
3
|
+
*
|
|
4
|
+
* Every frame it overlap-tests a circle around the player against the
|
|
5
|
+
* `core:Hitbox2D` groups listed in `rules`, and for each hit emits a SEMANTIC
|
|
6
|
+
* signal on its own node (`game-root`):
|
|
7
|
+
*
|
|
8
|
+
* `touch-scored` (amount, x, y)
|
|
9
|
+
* `touch-damaged` (amount, x, y)
|
|
10
|
+
*
|
|
11
|
+
* It never touches the score, the lives or the HUD — `GameRules` owns those and
|
|
12
|
+
* listens for these two signals. Add an outcome by adding a rule here and a
|
|
13
|
+
* listener there; nothing else has to change.
|
|
14
|
+
*
|
|
15
|
+
* `rules` format: `group:outcome:amount`, entries separated by `;`.
|
|
16
|
+
* Outcomes: `score` | `damage`. Example: `pickup:score:1;hazard:damage:1`.
|
|
17
|
+
*/
|
|
18
|
+
import { Script, type PropertySchema } from '@pix3/runtime';
|
|
19
|
+
|
|
20
|
+
type TouchOutcome = 'score' | 'damage';
|
|
21
|
+
|
|
22
|
+
interface TouchRule {
|
|
23
|
+
group: string;
|
|
24
|
+
outcome: TouchOutcome;
|
|
25
|
+
amount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseRules(text: string): TouchRule[] {
|
|
29
|
+
const rules: TouchRule[] = [];
|
|
30
|
+
for (const entry of text.split(';')) {
|
|
31
|
+
const parts = entry.trim().split(':');
|
|
32
|
+
if (parts.length < 2 || !parts[0].trim()) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const outcome: TouchOutcome = parts[1].trim() === 'damage' ? 'damage' : 'score';
|
|
36
|
+
const amount = Number(parts[2] ?? 1);
|
|
37
|
+
rules.push({
|
|
38
|
+
group: parts[0].trim(),
|
|
39
|
+
outcome,
|
|
40
|
+
amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return rules;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class TouchRules extends Script {
|
|
47
|
+
constructor(id: string, type: string) {
|
|
48
|
+
super(id, type);
|
|
49
|
+
this.config = {
|
|
50
|
+
// Node id/name whose world position is the centre of the overlap test.
|
|
51
|
+
playerNode: 'player',
|
|
52
|
+
// Radius of the overlap test, in logical pixels.
|
|
53
|
+
touchRadius: 62,
|
|
54
|
+
// group:outcome:amount pairs, separated by ';'.
|
|
55
|
+
rules: 'pickup:score:1;hazard:damage:1',
|
|
56
|
+
// Free the touched node after resolving it.
|
|
57
|
+
consume: true,
|
|
58
|
+
// res:// audio paths; empty = silent.
|
|
59
|
+
scoreSound: '',
|
|
60
|
+
damageSound: '',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static getPropertySchema(): PropertySchema {
|
|
65
|
+
return {
|
|
66
|
+
nodeType: 'TouchRules',
|
|
67
|
+
properties: [
|
|
68
|
+
{
|
|
69
|
+
name: 'playerNode',
|
|
70
|
+
type: 'string',
|
|
71
|
+
ui: { label: 'Player Node', group: 'Contact' },
|
|
72
|
+
getValue: s => (s as TouchRules).config.playerNode,
|
|
73
|
+
setValue: (s, v) => {
|
|
74
|
+
(s as TouchRules).config.playerNode = typeof v === 'string' ? v : '';
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'touchRadius',
|
|
79
|
+
type: 'number',
|
|
80
|
+
ui: { label: 'Touch Radius', group: 'Contact', min: 4, max: 600, step: 1, slider: true },
|
|
81
|
+
getValue: s => (s as TouchRules).config.touchRadius,
|
|
82
|
+
setValue: (s, v) => {
|
|
83
|
+
const n = Number(v);
|
|
84
|
+
(s as TouchRules).config.touchRadius = Math.min(600, Math.max(4, Number.isFinite(n) ? n : 4));
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'rules',
|
|
89
|
+
type: 'string',
|
|
90
|
+
ui: {
|
|
91
|
+
label: 'Rules',
|
|
92
|
+
description: "group:outcome:amount, ';'-separated. Outcome is score or damage.",
|
|
93
|
+
group: 'Contact',
|
|
94
|
+
},
|
|
95
|
+
getValue: s => (s as TouchRules).config.rules,
|
|
96
|
+
setValue: (s, v) => {
|
|
97
|
+
(s as TouchRules).config.rules = typeof v === 'string' ? v : '';
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'consume',
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
ui: { label: 'Consume On Touch', group: 'Contact' },
|
|
104
|
+
getValue: s => (s as TouchRules).config.consume,
|
|
105
|
+
setValue: (s, v) => {
|
|
106
|
+
(s as TouchRules).config.consume = Boolean(v);
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'scoreSound',
|
|
111
|
+
type: 'string',
|
|
112
|
+
ui: { label: 'Score Sound', group: 'Feedback', editor: 'audio-resource' },
|
|
113
|
+
getValue: s => (s as TouchRules).config.scoreSound,
|
|
114
|
+
setValue: (s, v) => {
|
|
115
|
+
(s as TouchRules).config.scoreSound = typeof v === 'string' ? v : '';
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'damageSound',
|
|
120
|
+
type: 'string',
|
|
121
|
+
ui: { label: 'Damage Sound', group: 'Feedback', editor: 'audio-resource' },
|
|
122
|
+
getValue: s => (s as TouchRules).config.damageSound,
|
|
123
|
+
setValue: (s, v) => {
|
|
124
|
+
(s as TouchRules).config.damageSound = typeof v === 'string' ? v : '';
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
groups: {
|
|
129
|
+
Contact: { label: 'Contact', expanded: true },
|
|
130
|
+
Feedback: { label: 'Feedback', expanded: false },
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onUpdate(): void {
|
|
136
|
+
const scene = this.scene;
|
|
137
|
+
const owner = this.node;
|
|
138
|
+
const player = this.findNode(String(this.config.playerNode ?? ''));
|
|
139
|
+
if (!scene || !owner || !player) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
player.updateWorldMatrix(true, false);
|
|
144
|
+
const px = player.matrixWorld.elements[12];
|
|
145
|
+
const py = player.matrixWorld.elements[13];
|
|
146
|
+
const radius = Math.max(1, Number(this.config.touchRadius) || 1);
|
|
147
|
+
const consume = Boolean(this.config.consume);
|
|
148
|
+
|
|
149
|
+
for (const rule of parseRules(String(this.config.rules ?? ''))) {
|
|
150
|
+
for (const hit of scene.collision2d.overlapCircle(px, py, radius, rule.group)) {
|
|
151
|
+
if (rule.outcome === 'damage') {
|
|
152
|
+
owner.emit('touch-damaged', rule.amount, hit.x, hit.y);
|
|
153
|
+
scene.juice.shake(player, { amplitude: 16, duration: 0.25 });
|
|
154
|
+
scene.juice.flash({ color: '#ff4d6d', intensity: 0.4, durationSec: 0.22 });
|
|
155
|
+
this.playSound(String(this.config.damageSound ?? ''));
|
|
156
|
+
} else {
|
|
157
|
+
owner.emit('touch-scored', rule.amount, hit.x, hit.y);
|
|
158
|
+
scene.juice.punchScale(player, { amount: 0.22, duration: 0.22 });
|
|
159
|
+
this.playSound(String(this.config.scoreSound ?? ''));
|
|
160
|
+
}
|
|
161
|
+
if (consume) {
|
|
162
|
+
hit.node.queueFree();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private playSound(path: string): void {
|
|
169
|
+
if (!path) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
void this.scene?.audio.play(path, { bus: 'sfx', pitchVariation: 0.08 });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
id: recipe-arena-2d
|
|
2
|
+
title: Arena 2D
|
|
3
|
+
description: An avatar you steer inside a bounded field while spawners drip pickups and hazards; touch turns into score or damage, with lives, a timer and a win/lose flow. Dodgers, collectors, top-down survival, and (by swapping the movement script) runners. Its steering is continuous, so a grid or turn-based game (snake, sokoban) starts from Blank 2D instead.
|
|
4
|
+
projectType: 2d
|
|
5
|
+
targetPlatform: universal
|
|
6
|
+
viewport: { width: 1080, height: 1920 }
|
|
7
|
+
# The build/full-run entry scene (Project Settings → Default Export Scene Path).
|
|
8
|
+
# The menu boots first; PLAY transitions to main.pix3scene (the game).
|
|
9
|
+
entryScene: scenes/menu.pix3scene
|
|
10
|
+
order: 60
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Blank 2D
|
|
2
|
+
|
|
3
|
+
Everything a run needs **except the mechanic**: a bounded field, a HUD, score /
|
|
4
|
+
lives / timer bookkeeping, and a win/lose screen with RETRY. Press play in
|
|
5
|
+
`scenes/main.pix3scene` and you get an empty field with `SCORE 0` — that is
|
|
6
|
+
correct, and it is the point.
|
|
7
|
+
|
|
8
|
+
Use this when the game's core loop is not what another recipe ships (grid or
|
|
9
|
+
turn-based movement, word / card / board games, builders, idle). The first thing
|
|
10
|
+
to build is the mechanic itself, controls included; building it here beats
|
|
11
|
+
deleting somebody else's mechanic first. A falling, bouncing ball is the bouncer
|
|
12
|
+
recipe's job — do not rebuild that here.
|
|
13
|
+
|
|
14
|
+
The field is not grey: `post-fx` blooms anything bright and `bg-glow` lights the
|
|
15
|
+
background, and `sprites/ph-*.png` is a shape library (disc, orb, ring, star,
|
|
16
|
+
capsule) tinted with the palette at T0. Build the mechanic out of those shapes,
|
|
17
|
+
not out of `ColorRect2D` squares — see `design/recipe.md` → Placeholders.
|
|
18
|
+
|
|
19
|
+
**`design/recipe.md` is the contract.** It lists the stable node ids, the
|
|
20
|
+
tunables (with their ranges), the extension points and what must not be renamed.
|
|
21
|
+
Read it first; tools and agents grep it.
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
scenes/main.pix3scene the game — open and build here. There is no menu on purpose.
|
|
27
|
+
scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
|
|
28
|
+
scripts/GameRules.ts score / lives / timer / win / lose / end flow / restart
|
|
29
|
+
scripts/ScoreHud.ts signals → HUD widgets (display only)
|
|
30
|
+
sprites/ph-*.png shape library (near-white, tinted via core:tint)
|
|
31
|
+
design/recipe.md the contract
|
|
32
|
+
design/tests/ reachability ledger + one example routine (a format seed)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How your mechanic plugs in
|
|
36
|
+
|
|
37
|
+
Write a script, attach it to a node inside `board`, and score through
|
|
38
|
+
`GameRules` — either by emitting on `game-root`:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
this.findNode('game-root')?.emit('score-added', 1);
|
|
42
|
+
this.findNode('game-root')?.emit('life-lost', 1);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
…or by calling the component directly (`addScore`, `loseLife`, `finish`). The
|
|
46
|
+
HUD, the end screen and RETRY then work with no further wiring.
|
|
47
|
+
|
|
48
|
+
## The rule that keeps it workable
|
|
49
|
+
|
|
50
|
+
One file, one responsibility. Keep each script in the 70–140 line range: small
|
|
51
|
+
files can be edited surgically; monoliths get rewritten wholesale and lose the
|
|
52
|
+
comments that explained them. When a mechanic outgrows its file, split it rather
|
|
53
|
+
than growing it.
|
|
54
|
+
|
|
55
|
+
## No menu yet
|
|
56
|
+
|
|
57
|
+
A menu is a screen between you and the thing you are working on. Add one only
|
|
58
|
+
once the game is fun: create `scenes/menu.pix3scene`, have its PLAY button
|
|
59
|
+
`scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`, then
|
|
60
|
+
point Project Settings → Default Export Scene Path at it. `Play Scene` keeps
|
|
61
|
+
running whatever scene is open, so the menu never gets in the way again.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Recipe: recipe-blank-2d
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
**Nothing plays yet, on purpose.** A bounded field, a HUD, and score / lives /
|
|
6
|
+
timer / win-lose bookkeeping that already works — and no mechanic of any kind,
|
|
7
|
+
including controls.
|
|
8
|
+
|
|
9
|
+
Pick this recipe when the idea's core loop is not what another recipe ships:
|
|
10
|
+
grid or turn-based movement (snake, sokoban, match-3), word / card / board games,
|
|
11
|
+
builders, idle games without a ball. **Your first increment is the core
|
|
12
|
+
mechanic itself, controls included** — that is the trade being made here, and it
|
|
13
|
+
beats spending the first increment deleting somebody else's mechanic.
|
|
14
|
+
|
|
15
|
+
Everything *around* the mechanic is done: you never hand-roll a score counter, a
|
|
16
|
+
lives bar, a timer, an end screen or a retry. The look is done too: `post-fx`
|
|
17
|
+
blooms anything bright and `bg-glow` lights the field, so a sprite tinted with a
|
|
18
|
+
palette accent already glows on the first frame.
|
|
19
|
+
|
|
20
|
+
## Node map
|
|
21
|
+
|
|
22
|
+
| id | what |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `post-fx` | `PostProcess` (`affect2D`): bloom + vignette — tune, never delete |
|
|
25
|
+
| `game-root` | root; hosts `GameRules`; every HUD signal is emitted here |
|
|
26
|
+
| `game-background` | full-screen `ColorRect2D` (palette background) |
|
|
27
|
+
| `bg-glow` | full-screen `Sprite2D`, the tinted radial gradient over it |
|
|
28
|
+
| `board` | the play field (`Group2D`) — **build your mechanic inside this** |
|
|
29
|
+
| `board-floor` | the field's visible plate; its rect is the field's extent |
|
|
30
|
+
| `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
|
|
31
|
+
| `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
|
|
32
|
+
| `result-overlay` / `result-dim` / `result-label` / `retry-button` | end screen, own file `scenes/ui/result.pix3scene`; instanced into main with `visible: false` (editor-only hide — `GameRules` shows it) |
|
|
33
|
+
|
|
34
|
+
Signals on `game-root`: your mechanic emits `score-added` (amount) and
|
|
35
|
+
`life-lost` (amount); `GameRules` answers `score-changed`, `lives-changed`,
|
|
36
|
+
`time-changed`, and `game-won` / `game-lost`.
|
|
37
|
+
|
|
38
|
+
There is **one gameplay scene** (`scenes/main.pix3scene`) and no menu — see
|
|
39
|
+
Extension points if the game eventually needs one.
|
|
40
|
+
|
|
41
|
+
## Placeholders
|
|
42
|
+
|
|
43
|
+
A **shape library**, not entities: near-white PNGs already tinted with the
|
|
44
|
+
palette at T0 (the role column decides the colour). Put them on a `Sprite2D`
|
|
45
|
+
sized to the thing they stand for, and recolour with `effects: [{type: core:tint,
|
|
46
|
+
params: {color}}]` when you need another accent. **A round thing is
|
|
47
|
+
`ph-circle`/`ph-orb`, a coin or bumper is `ph-ring`, a bar or paddle is
|
|
48
|
+
`ph-capsule`, a spark or star is `ph-star`.** A `ColorRect2D` is for panels,
|
|
49
|
+
floors and bars — never for a ball, a coin, an enemy or anything the player
|
|
50
|
+
looks at: a square ball on the first frame is what the user remembers. Real art
|
|
51
|
+
replaces these through `generate_asset` in the art pass.
|
|
52
|
+
|
|
53
|
+
| role | file | node/prefab |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| background | `sprites/ph-bg.png` | `bg-glow` (radial gradient) |
|
|
56
|
+
| player | `sprites/ph-circle.png` | (library — a solid disc with a soft edge) |
|
|
57
|
+
| avatar | `sprites/ph-orb.png` | (library — a disc with a bright rim) |
|
|
58
|
+
| collectible | `sprites/ph-ring.png` | (library — a ring; bumpers, coins, targets) |
|
|
59
|
+
| hazard | `sprites/ph-star.png` | (library — a four-point spark) |
|
|
60
|
+
| ui | `sprites/ph-capsule.png` | (library — a rounded bar; paddles, platforms, pills) |
|
|
61
|
+
|
|
62
|
+
## Tunables
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
tunables:
|
|
66
|
+
winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
|
|
67
|
+
targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
|
|
68
|
+
timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
|
|
69
|
+
startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
|
|
70
|
+
bgColor: { node: game-background, property: color, default: "#12141c" }
|
|
71
|
+
boardColor: { node: board-floor, property: color, default: "#1d212e" }
|
|
72
|
+
bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.8 }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`component` present → `set_component_property`; absent → `set_property`. The
|
|
76
|
+
schema clamps out-of-range values rather than rejecting them.
|
|
77
|
+
|
|
78
|
+
`timeLimitSec: 0` means **no clock**: the run ends on the target score or on
|
|
79
|
+
running out of lives, and `time-label` counts elapsed seconds up. Set a limit and
|
|
80
|
+
it counts down and becomes a deadline (see `winMode`).
|
|
81
|
+
|
|
82
|
+
## Extension points
|
|
83
|
+
|
|
84
|
+
- **The mechanic.** Write a script under `scripts/`, attach it to a node inside
|
|
85
|
+
`board`, and let it own input and movement. It scores through `GameRules` —
|
|
86
|
+
either `node.emit('score-added', 1)` on `game-root`, or a direct
|
|
87
|
+
`addScore(1)` / `loseLife()` / `finish(true)` on the component. Nothing else in
|
|
88
|
+
the project needs to change for the HUD and the end screen to work.
|
|
89
|
+
- **Collision, when the mechanic needs it.** Three tiers, cheapest first. Overlap
|
|
90
|
+
queries with no response: `scene.collision2d` + `core:Hitbox2D` (axis-aligned).
|
|
91
|
+
Bouncing, pushing, gravity, sensors: the engine's own 2D rigid-body solver —
|
|
92
|
+
`core:PhysicsBody2D` + `core:Collider2D` on the same node (a `core:Collider2D`
|
|
93
|
+
alone is static world geometry), `scene.physics2d.getBody(node)` for
|
|
94
|
+
`applyImpulse` / `setVelocity` / `teleport`, `setGravity(0, 0)` for top-down.
|
|
95
|
+
**Never hand-write a 2D solver, and do not import rapier** (3D only, ~2 MB wasm).
|
|
96
|
+
Grid games usually need no collision at all — compare cell coordinates.
|
|
97
|
+
- **Spawning.** `scene.instantiate` a prefab into a container node, `queueFree`
|
|
98
|
+
it when it leaves the field, and add that container to `GameRules.freezeNodes`
|
|
99
|
+
so it — and every instance under it — stops on game over. Give your spawner a `clear()` method and `resetRun()`
|
|
100
|
+
will empty the field for you.
|
|
101
|
+
- **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
|
|
102
|
+
`scene.juice.burst({x, y})`, `floatText('+5', {at: node})`, `punchScale` /
|
|
103
|
+
`shake` / `flash`, `scene.audio.sfx('score')`, `scene.time.hitstop(50)` on a
|
|
104
|
+
contact START; `scene.juice.trail(ball)` for a light streak; `scene.tween.to(node,
|
|
105
|
+
{scale: 1.2}, {durationSec: 0.15, yoyo: true, repeat: 1})` / `crossFade(a, b)` for any
|
|
106
|
+
motion or panel switch instead of hand-lerping. Bright sprites bloom through `post-fx`
|
|
107
|
+
by themselves.
|
|
108
|
+
- **A menu, later.** Create `scenes/menu.pix3scene` with a root carrying a script
|
|
109
|
+
that calls `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`
|
|
110
|
+
on PLAY, then set Project Settings → Default Export Scene Path to it. Do this
|
|
111
|
+
**after** the game is fun, never before: while iterating, the menu is a screen
|
|
112
|
+
between you and the thing you are working on.
|
|
113
|
+
|
|
114
|
+
## Ending a run belongs to GameRules
|
|
115
|
+
|
|
116
|
+
The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
|
|
117
|
+
start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
|
|
118
|
+
writes the result text, enables RETRY and freezes the `freezeNodes` nodes. For a
|
|
119
|
+
custom win/lose condition call `finish(true|false)` — do NOT show the overlay
|
|
120
|
+
yourself. A hand-rolled ending leaves RETRY on screen with its handler bound and
|
|
121
|
+
`enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
|
|
122
|
+
`control: { enabled: false }`; that is the tell).
|
|
123
|
+
|
|
124
|
+
## Do not touch
|
|
125
|
+
|
|
126
|
+
- The node ids above (rename `name`, never `id`), and the signal names.
|
|
127
|
+
- `GameRules`' ownership of `result-overlay` and `retry-button`.
|
|
128
|
+
- `post-fx`: keep the id and `affect2D: true` — without it a 2D scene gets no
|
|
129
|
+
bloom. Tune the values, don't delete the node.
|
|
130
|
+
- Do not import rapier, and do not add a menu scene early.
|
|
131
|
+
|
|
132
|
+
## Playtest contract
|
|
133
|
+
|
|
134
|
+
terminalVisual: static
|
|
135
|
+
terminalRestartRoutine: terminal-retry
|
|
136
|
+
|
|
137
|
+
## Verify
|
|
138
|
+
|
|
139
|
+
1. `play_start` on `scenes/main.pix3scene` — `game_observe` reports the snapshot
|
|
140
|
+
with `phase: playing`, `score: 0`, `lives: 3`. That is the baseline, before any
|
|
141
|
+
mechanic exists.
|
|
142
|
+
2. After the first mechanic increment, prove it by **state delta**, not by a
|
|
143
|
+
screenshot: `game_input` the real input with `observe:` on the node that should
|
|
144
|
+
react, and read `moved` / `activity` / the `game.changed` diff.
|
|
145
|
+
3. Scoring is proven the same way: one real input, then `score` up in the snapshot
|
|
146
|
+
(and `score-label` reading `SCORE 1`).
|
|
147
|
+
4. `targetScore: 1`, replay, score once → `result-overlay` visible with `YOU WIN!`;
|
|
148
|
+
`retry-button` (or the `restart` intent) starts a fresh run.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "restart",
|
|
3
|
+
"description": "Dispatch the run's `restart` intent and confirm a fresh run came back up.",
|
|
4
|
+
"note": "This is a FORMAT SEED, not a real test of your game — there is no mechanic here yet to test. Keep it (restart stays worth proving after a scene reload) and add a routine per mechanic next to it: run one with game_run {routine:'<name>'}. Steps are the game_input vocabulary plus {type:'command'} for a registered intent; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step against the frame pair around the run. Only a STEP takes a `why`. Prefer a `gameState` predicate over a node one wherever GameRules already publishes the value — it survives every relayout of the HUD.",
|
|
5
|
+
"scope": "scenes/main.pix3scene",
|
|
6
|
+
"params": {},
|
|
7
|
+
"uses": ["game-root", "result-overlay", "retry-button"],
|
|
8
|
+
"steps": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"name": "restart",
|
|
12
|
+
"why": "The intent RETRY dispatches, taken at the highest channel: it proves the wire without depending on where the button sits, and the dispatch is journalled with the frame it happened on."
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"expect": [
|
|
16
|
+
{ "kind": "command", "name": "restart" },
|
|
17
|
+
{ "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" },
|
|
18
|
+
{ "kind": "gameState", "path": "score", "op": "eq", "value": 0 },
|
|
19
|
+
{
|
|
20
|
+
"kind": "nodeProperty",
|
|
21
|
+
"name": "result-overlay",
|
|
22
|
+
"path": "visible",
|
|
23
|
+
"op": "eq",
|
|
24
|
+
"value": false
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "terminal-retry",
|
|
3
|
+
"description": "From the result screen, physically tap RETRY and check that play resumes.",
|
|
4
|
+
"scope": "scenes/main.pix3scene",
|
|
5
|
+
"params": {},
|
|
6
|
+
"uses": ["result-overlay", "retry-button"],
|
|
7
|
+
"steps": [
|
|
8
|
+
{
|
|
9
|
+
"type": "tap",
|
|
10
|
+
"target": "retry-button",
|
|
11
|
+
"holdMs": 200,
|
|
12
|
+
"why": "A real pointer press proves the button is visible, enabled and reachable."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "wait",
|
|
16
|
+
"frames": 30,
|
|
17
|
+
"why": "Let the scene transition finish before reading the new game's phase."
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"expect": [
|
|
21
|
+
{ "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Pix3 Scene File (YAML)
|
|
2
|
+
# Blank 2D — the GAME scene, and the one to build in. There is no menu scene on
|
|
3
|
+
# purpose: nothing should stand between opening the project and the gameplay
|
|
4
|
+
# while the mechanic is still being found.
|
|
5
|
+
#
|
|
6
|
+
# What is already wired: score / lives / timer bookkeeping, the HUD that shows
|
|
7
|
+
# them, and a win/lose overlay with RETRY. What is NOT here: any mechanic at all,
|
|
8
|
+
# including controls. That is your first increment — build it inside `board`.
|
|
9
|
+
#
|
|
10
|
+
# Stable node ids (design/recipe.md is the contract — do not rename these):
|
|
11
|
+
# post-fx, game-root, game-background, bg-glow, board, board-floor,
|
|
12
|
+
# hud, score-label, time-label, lives-bar,
|
|
13
|
+
# result-overlay, result-dim, result-label, retry-button (scenes/ui/result.pix3scene)
|
|
14
|
+
#
|
|
15
|
+
# The look is two cheap ingredients, the same ones the bouncer recipe uses:
|
|
16
|
+
# 1. `post-fx` (PostProcess, affect2D) — bloom picks up whatever is bright, so
|
|
17
|
+
# a bright sprite glows for free. `bloomIntensity` is a recipe tunable the
|
|
18
|
+
# Flow theme pack turns up for neon and down for pastel.
|
|
19
|
+
# 2. `bg-glow` — a near-white radial-gradient placeholder (`sprites/ph-bg.png`)
|
|
20
|
+
# tinted with the palette background, over the darkest ColorRect. It is what
|
|
21
|
+
# keeps an empty field from reading as a grey slab.
|
|
22
|
+
#
|
|
23
|
+
# The win/lose overlay lives in its own file under scenes/ui/ and is instanced
|
|
24
|
+
# here hidden (`visible: false` = editor only), so opening this scene shows the
|
|
25
|
+
# field rather than a GAME OVER card.
|
|
26
|
+
#
|
|
27
|
+
# 2D coordinates: origin at the canvas centre, X right, Y UP. Node order in the
|
|
28
|
+
# tree is paint order (later/deeper draws on top).
|
|
29
|
+
version: 1.0.0
|
|
30
|
+
metadata:
|
|
31
|
+
author: Pix3 Recipe
|
|
32
|
+
description: Blank gameplay field — bookkeeping, HUD and win/lose only; the mechanic is yours
|
|
33
|
+
root:
|
|
34
|
+
- id: post-fx
|
|
35
|
+
type: PostProcess
|
|
36
|
+
name: Post FX
|
|
37
|
+
properties:
|
|
38
|
+
affect2D: true
|
|
39
|
+
bloomEnabled: true
|
|
40
|
+
bloomIntensity: 0.8
|
|
41
|
+
bloomThreshold: 0.6
|
|
42
|
+
bloomSmoothing: 0.2
|
|
43
|
+
bloomRadius: 0.75
|
|
44
|
+
vignetteEnabled: true
|
|
45
|
+
vignetteOffset: 0.3
|
|
46
|
+
vignetteDarkness: 0.45
|
|
47
|
+
chromaticAberrationEnabled: false
|
|
48
|
+
children: []
|
|
49
|
+
- id: game-root
|
|
50
|
+
type: Group2D
|
|
51
|
+
name: Game Root
|
|
52
|
+
properties:
|
|
53
|
+
width: 1080
|
|
54
|
+
height: 1920
|
|
55
|
+
layout:
|
|
56
|
+
enabled: true
|
|
57
|
+
horizontalAlign: stretch
|
|
58
|
+
verticalAlign: stretch
|
|
59
|
+
components:
|
|
60
|
+
- id: game-rules
|
|
61
|
+
type: user:GameRules
|
|
62
|
+
enabled: true
|
|
63
|
+
config:
|
|
64
|
+
winMode: score
|
|
65
|
+
targetScore: 10
|
|
66
|
+
timeLimitSec: 0
|
|
67
|
+
startingLives: 3
|
|
68
|
+
resultNode: result-overlay
|
|
69
|
+
resultLabel: result-label
|
|
70
|
+
winText: "YOU WIN!"
|
|
71
|
+
loseText: "GAME OVER"
|
|
72
|
+
retryButton: retry-button
|
|
73
|
+
gameScene: res://scenes/main.pix3scene
|
|
74
|
+
# Add your mechanic's nodes here (comma-separated) so they stop on game over.
|
|
75
|
+
freezeNodes: ""
|
|
76
|
+
children:
|
|
77
|
+
- id: game-background
|
|
78
|
+
type: ColorRect2D
|
|
79
|
+
name: Game Background
|
|
80
|
+
properties:
|
|
81
|
+
width: 1080
|
|
82
|
+
height: 1920
|
|
83
|
+
color: "#12141c"
|
|
84
|
+
transform:
|
|
85
|
+
position: [0, 0]
|
|
86
|
+
scale: [1, 1]
|
|
87
|
+
rotation: 0
|
|
88
|
+
layout:
|
|
89
|
+
enabled: true
|
|
90
|
+
horizontalAlign: stretch
|
|
91
|
+
verticalAlign: stretch
|
|
92
|
+
children: []
|
|
93
|
+
# Radial glow over the flat background. Near-white PNG + core:tint, so the
|
|
94
|
+
# T0 palette recolours it (role `background` in design/recipe.md).
|
|
95
|
+
- id: bg-glow
|
|
96
|
+
type: Sprite2D
|
|
97
|
+
name: Background Glow
|
|
98
|
+
properties:
|
|
99
|
+
texture: { type: 'texture', url: 'res://sprites/ph-bg.png' }
|
|
100
|
+
width: 1080
|
|
101
|
+
height: 1920
|
|
102
|
+
opacity: 0.85
|
|
103
|
+
effects:
|
|
104
|
+
- type: core:tint
|
|
105
|
+
params: { color: "#3a2f7a", amount: 1 }
|
|
106
|
+
transform:
|
|
107
|
+
position: [0, 0]
|
|
108
|
+
scale: [1, 1]
|
|
109
|
+
rotation: 0
|
|
110
|
+
layout:
|
|
111
|
+
enabled: true
|
|
112
|
+
horizontalAlign: stretch
|
|
113
|
+
verticalAlign: stretch
|
|
114
|
+
children: []
|
|
115
|
+
- id: board
|
|
116
|
+
type: Group2D
|
|
117
|
+
name: Board
|
|
118
|
+
properties:
|
|
119
|
+
width: 980
|
|
120
|
+
height: 1480
|
|
121
|
+
transform:
|
|
122
|
+
position: [0, -80]
|
|
123
|
+
scale: [1, 1]
|
|
124
|
+
rotation: 0
|
|
125
|
+
children:
|
|
126
|
+
- id: board-floor
|
|
127
|
+
type: ColorRect2D
|
|
128
|
+
name: Board Floor
|
|
129
|
+
properties:
|
|
130
|
+
width: 980
|
|
131
|
+
height: 1480
|
|
132
|
+
color: "#1d212e"
|
|
133
|
+
transform:
|
|
134
|
+
position: [0, 0]
|
|
135
|
+
scale: [1, 1]
|
|
136
|
+
rotation: 0
|
|
137
|
+
children: []
|
|
138
|
+
- id: hud
|
|
139
|
+
type: CanvasLayer2D
|
|
140
|
+
name: HUD
|
|
141
|
+
properties:
|
|
142
|
+
width: 1080
|
|
143
|
+
height: 1920
|
|
144
|
+
layout:
|
|
145
|
+
enabled: true
|
|
146
|
+
horizontalAlign: stretch
|
|
147
|
+
verticalAlign: stretch
|
|
148
|
+
components:
|
|
149
|
+
- id: score-hud
|
|
150
|
+
type: user:ScoreHud
|
|
151
|
+
enabled: true
|
|
152
|
+
config:
|
|
153
|
+
sourceNode: game-root
|
|
154
|
+
scoreLabel: score-label
|
|
155
|
+
scoreFormat: "SCORE {value}"
|
|
156
|
+
timeLabel: time-label
|
|
157
|
+
timeFormat: "{value}"
|
|
158
|
+
livesBar: lives-bar
|
|
159
|
+
children:
|
|
160
|
+
- id: score-label
|
|
161
|
+
type: Label2D
|
|
162
|
+
name: Score Label
|
|
163
|
+
properties:
|
|
164
|
+
label: "SCORE 0"
|
|
165
|
+
labelFontSize: 44
|
|
166
|
+
labelColor: "#ffffff"
|
|
167
|
+
labelAlign: left
|
|
168
|
+
transform:
|
|
169
|
+
position: [-340, 850]
|
|
170
|
+
scale: [1, 1]
|
|
171
|
+
rotation: 0
|
|
172
|
+
layout:
|
|
173
|
+
enabled: true
|
|
174
|
+
horizontalAlign: left
|
|
175
|
+
verticalAlign: top
|
|
176
|
+
children: []
|
|
177
|
+
- id: time-label
|
|
178
|
+
type: Label2D
|
|
179
|
+
name: Time Label
|
|
180
|
+
properties:
|
|
181
|
+
label: "0"
|
|
182
|
+
labelFontSize: 44
|
|
183
|
+
labelColor: "#f5ae39"
|
|
184
|
+
labelAlign: center
|
|
185
|
+
transform:
|
|
186
|
+
position: [0, 850]
|
|
187
|
+
scale: [1, 1]
|
|
188
|
+
rotation: 0
|
|
189
|
+
layout:
|
|
190
|
+
enabled: true
|
|
191
|
+
horizontalAlign: center
|
|
192
|
+
verticalAlign: top
|
|
193
|
+
children: []
|
|
194
|
+
- id: lives-bar
|
|
195
|
+
type: Bar2D
|
|
196
|
+
name: Lives Bar
|
|
197
|
+
properties:
|
|
198
|
+
width: 240
|
|
199
|
+
height: 28
|
|
200
|
+
minValue: 0
|
|
201
|
+
maxValue: 3
|
|
202
|
+
value: 3
|
|
203
|
+
barColor: "#ff4d6d"
|
|
204
|
+
backBackgroundColor: "#2a3352"
|
|
205
|
+
showBorder: false
|
|
206
|
+
transform:
|
|
207
|
+
position: [340, 855]
|
|
208
|
+
scale: [1, 1]
|
|
209
|
+
rotation: 0
|
|
210
|
+
layout:
|
|
211
|
+
enabled: true
|
|
212
|
+
horizontalAlign: right
|
|
213
|
+
verticalAlign: top
|
|
214
|
+
children: []
|
|
215
|
+
# Win/lose screen — authored in scenes/ui/result.pix3scene so this
|
|
216
|
+
# scene opens on the GAME, not on a GAME OVER card. `visible: false`
|
|
217
|
+
# hides it in the EDITOR only; `initiallyVisible` (in the overlay
|
|
218
|
+
# file) is what play mode reads, and GameRules reveals it on the
|
|
219
|
+
# win/lose transition. Tick the eye to preview it over the field.
|
|
220
|
+
- id: result-overlay
|
|
221
|
+
name: Result Overlay
|
|
222
|
+
instance: res://scenes/ui/result.pix3scene
|
|
223
|
+
properties:
|
|
224
|
+
visible: false
|