@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,350 +1,350 @@
|
|
|
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
|
+
/**
|
|
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
|
+
}
|