@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,217 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MenuFlow — drives the menu scene.
|
|
3
|
-
*
|
|
4
|
-
* Attach to the menu scene root. PLAY transitions to the game scene via a fade
|
|
5
|
-
* (`this.scene.changeScene`), SETTINGS shows the settings-window prefab instance.
|
|
6
|
-
* The game lives in its own scene (main.pix3scene) so it can be opened and played
|
|
7
|
-
* on its own; this menu is the entry point wired into the full build flow.
|
|
8
|
-
*
|
|
9
|
-
* Handlers here are **intent-first**: each button's behaviour lives in a named
|
|
10
|
-
* public method marked `Intent:`, and the intent is registered as a **command**
|
|
11
|
-
* (`scene.commands`) that the button's signal handler dispatches. Three things
|
|
12
|
-
* follow, all for free: a test drives the menu with
|
|
13
|
-
* `scene.commands.dispatch('start-game')` instead of a tap; every raised intent
|
|
14
|
-
* lands in `scene.commands.log` with the frame it happened on; and one real tap
|
|
15
|
-
* is enough to prove the button is still wired to the intent it claims.
|
|
16
|
-
*
|
|
17
|
-
* It also registers a debug provider, exactly like `GameFlow` does for the game
|
|
18
|
-
* scene. `registerGameDebug` is global and last-wins, and only the running
|
|
19
|
-
* scene's flow script registers one — so without this, every tool reading the
|
|
20
|
-
* game while the menu is up would get nothing (or stale game-scene state).
|
|
21
|
-
*/
|
|
22
|
-
import {
|
|
23
|
-
Checkbox2D,
|
|
24
|
-
NodeBase,
|
|
25
|
-
Script,
|
|
26
|
-
registerGameDebug,
|
|
27
|
-
type PropertySchema,
|
|
28
|
-
} from '@pix3/runtime';
|
|
29
|
-
|
|
30
|
-
export class MenuFlow extends Script {
|
|
31
|
-
private playButtonReady = false;
|
|
32
|
-
private settingsButtonReady = false;
|
|
33
|
-
private disposeDebug: (() => void) | null = null;
|
|
34
|
-
private disposeCommands: (() => void)[] = [];
|
|
35
|
-
|
|
36
|
-
constructor(id: string, type: string) {
|
|
37
|
-
super(id, type);
|
|
38
|
-
this.config = {
|
|
39
|
-
// res:// path of the scene PLAY transitions to.
|
|
40
|
-
gameScene: 'res://scenes/main.pix3scene',
|
|
41
|
-
// Node id/name of the settings-window prefab instance to toggle.
|
|
42
|
-
settingsNode: 'settings-window',
|
|
43
|
-
playButton: 'play-button',
|
|
44
|
-
settingsButton: 'settings-button',
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
static getPropertySchema(): PropertySchema {
|
|
49
|
-
const stringProp = (name: string, label: string) => ({
|
|
50
|
-
name,
|
|
51
|
-
type: 'string' as const,
|
|
52
|
-
ui: { label, group: 'Menu' },
|
|
53
|
-
getValue: (s: unknown) => (s as MenuFlow).config[name],
|
|
54
|
-
setValue: (s: unknown, v: unknown) => {
|
|
55
|
-
(s as MenuFlow).config[name] = typeof v === 'string' ? v : '';
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
nodeType: 'MenuFlow',
|
|
61
|
-
properties: [
|
|
62
|
-
stringProp('gameScene', 'Game Scene'),
|
|
63
|
-
stringProp('settingsNode', 'Settings Window'),
|
|
64
|
-
stringProp('playButton', 'Play Button'),
|
|
65
|
-
stringProp('settingsButton', 'Settings Button'),
|
|
66
|
-
],
|
|
67
|
-
groups: { Menu: { label: 'Menu Flow', expanded: true } },
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
onStart(): void {
|
|
72
|
-
// Register the intents first, then point the buttons at them: a button
|
|
73
|
-
// dispatches, it does not call the method, so the command journal is the
|
|
74
|
-
// whole truth about what the player asked for.
|
|
75
|
-
const commands = this.scene?.commands;
|
|
76
|
-
this.disposeCommands = [
|
|
77
|
-
commands?.register('start-game', () => this.startGame(), {
|
|
78
|
-
description: 'Leave the menu and transition into the game scene.',
|
|
79
|
-
}),
|
|
80
|
-
commands?.register(
|
|
81
|
-
'open-settings',
|
|
82
|
-
() => {
|
|
83
|
-
this.openSettings();
|
|
84
|
-
// Returning a reverse step is all it takes to make an intent
|
|
85
|
-
// undoable: `scene.commands.undo()` closes the window again.
|
|
86
|
-
return { undo: () => this.closeSettings() };
|
|
87
|
-
},
|
|
88
|
-
{ description: 'Show the settings window over the menu.', undoable: true }
|
|
89
|
-
),
|
|
90
|
-
commands?.register('close-settings', () => this.closeSettings(), {
|
|
91
|
-
description: 'Hide the settings window.',
|
|
92
|
-
}),
|
|
93
|
-
].filter((dispose): dispose is () => void => dispose !== undefined);
|
|
94
|
-
|
|
95
|
-
this.playButtonReady = this.connectButton(String(this.config.playButton ?? ''), () => {
|
|
96
|
-
this.scene?.commands.dispatch('start-game');
|
|
97
|
-
});
|
|
98
|
-
this.settingsButtonReady = this.connectButton(String(this.config.settingsButton ?? ''), () => {
|
|
99
|
-
this.scene?.commands.dispatch('open-settings');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
this.disposeDebug = registerGameDebug({
|
|
103
|
-
name: 'minigame-2d',
|
|
104
|
-
version: 1,
|
|
105
|
-
// The registry IS the action list — never a second, hand-kept copy.
|
|
106
|
-
actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
|
|
107
|
-
// What "the start" means for the menu: the fresh menu has the settings
|
|
108
|
-
// window closed, and an earlier `open-settings` is the one piece of state a
|
|
109
|
-
// later run would inherit. Deliberately NOT reset here: the checkboxes
|
|
110
|
-
// inside the window, which are player settings — a reset that silently
|
|
111
|
-
// reverted someone's audio choice would be a worse lie than not resetting.
|
|
112
|
-
// `seed` is ignored; the menu has no randomness to seed.
|
|
113
|
-
reset: () => this.closeSettings(),
|
|
114
|
-
// Same `name` as GameFlow — one game, one identity; `scene` says which
|
|
115
|
-
// flow answered. Everything here must stay JSON-serialisable.
|
|
116
|
-
snapshot: () => ({
|
|
117
|
-
scene: 'menu',
|
|
118
|
-
gameScene: String(this.config.gameScene ?? ''),
|
|
119
|
-
playButtonReady: this.playButtonReady,
|
|
120
|
-
settingsButtonReady: this.settingsButtonReady,
|
|
121
|
-
settingsVisible: this.isSettingsVisible(),
|
|
122
|
-
// Checkbox state inside the settings window, keyed by node id
|
|
123
|
-
// (`music-toggle` / `sfx-toggle` in the shipped prefab).
|
|
124
|
-
settingsToggles: this.readSettingsToggles(),
|
|
125
|
-
}),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
onDetach(): void {
|
|
130
|
-
// Safe in any order: the disposer clears the global only while it still
|
|
131
|
-
// holds *this* provider, so the game scene's registration is never wiped.
|
|
132
|
-
this.disposeDebug?.();
|
|
133
|
-
this.disposeDebug = null;
|
|
134
|
-
// The scene's registry is cleared on stop anyway; this covers the other
|
|
135
|
-
// case — this script being detached while the scene keeps running.
|
|
136
|
-
for (const dispose of this.disposeCommands) {
|
|
137
|
-
dispose();
|
|
138
|
-
}
|
|
139
|
-
this.disposeCommands = [];
|
|
140
|
-
this.playButtonReady = false;
|
|
141
|
-
this.settingsButtonReady = false;
|
|
142
|
-
super.onDetach();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Intent: leave the menu and transition into the game scene. */
|
|
146
|
-
startGame(): void {
|
|
147
|
-
const gameScene = String(this.config.gameScene ?? '');
|
|
148
|
-
if (!gameScene) {
|
|
149
|
-
console.warn('[MenuFlow] No game scene configured.');
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
void this.scene?.changeScene(gameScene, { transition: 'fade', durationSec: 0.3 });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Intent: show the settings window over the menu. */
|
|
156
|
-
openSettings(): void {
|
|
157
|
-
this.setNodeVisible(String(this.config.settingsNode ?? ''), true);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Intent: hide the settings window (its own CLOSE button does this too). */
|
|
161
|
-
closeSettings(): void {
|
|
162
|
-
this.setNodeVisible(String(this.config.settingsNode ?? ''), false);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Returns whether the button was found and wired (reported in the snapshot). */
|
|
166
|
-
private connectButton(query: string, handler: () => void): boolean {
|
|
167
|
-
if (!query) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
const button = this.findNode(query);
|
|
171
|
-
if (!button) {
|
|
172
|
-
console.warn(`[MenuFlow] Button "${query}" not found.`);
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
button.connect('pressed', this, handler);
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private isSettingsVisible(): boolean {
|
|
180
|
-
const node = this.findNode(String(this.config.settingsNode ?? ''));
|
|
181
|
-
return node?.visible === true;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Every Checkbox2D under the settings window, by node id → checked. */
|
|
185
|
-
private readSettingsToggles(): Record<string, boolean> {
|
|
186
|
-
const toggles: Record<string, boolean> = {};
|
|
187
|
-
const root = this.findNode(String(this.config.settingsNode ?? ''));
|
|
188
|
-
if (!root) {
|
|
189
|
-
return toggles;
|
|
190
|
-
}
|
|
191
|
-
const visit = (node: NodeBase): void => {
|
|
192
|
-
if (node instanceof Checkbox2D) {
|
|
193
|
-
toggles[node.nodeId] = node.checked;
|
|
194
|
-
}
|
|
195
|
-
for (const child of node.children) {
|
|
196
|
-
// `children` also holds plain Three.js objects (visuals), not just nodes.
|
|
197
|
-
if (child instanceof NodeBase) {
|
|
198
|
-
visit(child);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
visit(root);
|
|
203
|
-
return toggles;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private setNodeVisible(query: string, visible: boolean): void {
|
|
207
|
-
if (!query) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const node = this.findNode(query);
|
|
211
|
-
if (node) {
|
|
212
|
-
node.visible = visible;
|
|
213
|
-
} else {
|
|
214
|
-
console.warn(`[MenuFlow] Node "${query}" not found.`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MenuFlow — drives the menu scene.
|
|
3
|
+
*
|
|
4
|
+
* Attach to the menu scene root. PLAY transitions to the game scene via a fade
|
|
5
|
+
* (`this.scene.changeScene`), SETTINGS shows the settings-window prefab instance.
|
|
6
|
+
* The game lives in its own scene (main.pix3scene) so it can be opened and played
|
|
7
|
+
* on its own; this menu is the entry point wired into the full build flow.
|
|
8
|
+
*
|
|
9
|
+
* Handlers here are **intent-first**: each button's behaviour lives in a named
|
|
10
|
+
* public method marked `Intent:`, and the intent is registered as a **command**
|
|
11
|
+
* (`scene.commands`) that the button's signal handler dispatches. Three things
|
|
12
|
+
* follow, all for free: a test drives the menu with
|
|
13
|
+
* `scene.commands.dispatch('start-game')` instead of a tap; every raised intent
|
|
14
|
+
* lands in `scene.commands.log` with the frame it happened on; and one real tap
|
|
15
|
+
* is enough to prove the button is still wired to the intent it claims.
|
|
16
|
+
*
|
|
17
|
+
* It also registers a debug provider, exactly like `GameFlow` does for the game
|
|
18
|
+
* scene. `registerGameDebug` is global and last-wins, and only the running
|
|
19
|
+
* scene's flow script registers one — so without this, every tool reading the
|
|
20
|
+
* game while the menu is up would get nothing (or stale game-scene state).
|
|
21
|
+
*/
|
|
22
|
+
import {
|
|
23
|
+
Checkbox2D,
|
|
24
|
+
NodeBase,
|
|
25
|
+
Script,
|
|
26
|
+
registerGameDebug,
|
|
27
|
+
type PropertySchema,
|
|
28
|
+
} from '@pix3/runtime';
|
|
29
|
+
|
|
30
|
+
export class MenuFlow extends Script {
|
|
31
|
+
private playButtonReady = false;
|
|
32
|
+
private settingsButtonReady = false;
|
|
33
|
+
private disposeDebug: (() => void) | null = null;
|
|
34
|
+
private disposeCommands: (() => void)[] = [];
|
|
35
|
+
|
|
36
|
+
constructor(id: string, type: string) {
|
|
37
|
+
super(id, type);
|
|
38
|
+
this.config = {
|
|
39
|
+
// res:// path of the scene PLAY transitions to.
|
|
40
|
+
gameScene: 'res://scenes/main.pix3scene',
|
|
41
|
+
// Node id/name of the settings-window prefab instance to toggle.
|
|
42
|
+
settingsNode: 'settings-window',
|
|
43
|
+
playButton: 'play-button',
|
|
44
|
+
settingsButton: 'settings-button',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static getPropertySchema(): PropertySchema {
|
|
49
|
+
const stringProp = (name: string, label: string) => ({
|
|
50
|
+
name,
|
|
51
|
+
type: 'string' as const,
|
|
52
|
+
ui: { label, group: 'Menu' },
|
|
53
|
+
getValue: (s: unknown) => (s as MenuFlow).config[name],
|
|
54
|
+
setValue: (s: unknown, v: unknown) => {
|
|
55
|
+
(s as MenuFlow).config[name] = typeof v === 'string' ? v : '';
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
nodeType: 'MenuFlow',
|
|
61
|
+
properties: [
|
|
62
|
+
stringProp('gameScene', 'Game Scene'),
|
|
63
|
+
stringProp('settingsNode', 'Settings Window'),
|
|
64
|
+
stringProp('playButton', 'Play Button'),
|
|
65
|
+
stringProp('settingsButton', 'Settings Button'),
|
|
66
|
+
],
|
|
67
|
+
groups: { Menu: { label: 'Menu Flow', expanded: true } },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onStart(): void {
|
|
72
|
+
// Register the intents first, then point the buttons at them: a button
|
|
73
|
+
// dispatches, it does not call the method, so the command journal is the
|
|
74
|
+
// whole truth about what the player asked for.
|
|
75
|
+
const commands = this.scene?.commands;
|
|
76
|
+
this.disposeCommands = [
|
|
77
|
+
commands?.register('start-game', () => this.startGame(), {
|
|
78
|
+
description: 'Leave the menu and transition into the game scene.',
|
|
79
|
+
}),
|
|
80
|
+
commands?.register(
|
|
81
|
+
'open-settings',
|
|
82
|
+
() => {
|
|
83
|
+
this.openSettings();
|
|
84
|
+
// Returning a reverse step is all it takes to make an intent
|
|
85
|
+
// undoable: `scene.commands.undo()` closes the window again.
|
|
86
|
+
return { undo: () => this.closeSettings() };
|
|
87
|
+
},
|
|
88
|
+
{ description: 'Show the settings window over the menu.', undoable: true }
|
|
89
|
+
),
|
|
90
|
+
commands?.register('close-settings', () => this.closeSettings(), {
|
|
91
|
+
description: 'Hide the settings window.',
|
|
92
|
+
}),
|
|
93
|
+
].filter((dispose): dispose is () => void => dispose !== undefined);
|
|
94
|
+
|
|
95
|
+
this.playButtonReady = this.connectButton(String(this.config.playButton ?? ''), () => {
|
|
96
|
+
this.scene?.commands.dispatch('start-game');
|
|
97
|
+
});
|
|
98
|
+
this.settingsButtonReady = this.connectButton(String(this.config.settingsButton ?? ''), () => {
|
|
99
|
+
this.scene?.commands.dispatch('open-settings');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.disposeDebug = registerGameDebug({
|
|
103
|
+
name: 'minigame-2d',
|
|
104
|
+
version: 1,
|
|
105
|
+
// The registry IS the action list — never a second, hand-kept copy.
|
|
106
|
+
actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
|
|
107
|
+
// What "the start" means for the menu: the fresh menu has the settings
|
|
108
|
+
// window closed, and an earlier `open-settings` is the one piece of state a
|
|
109
|
+
// later run would inherit. Deliberately NOT reset here: the checkboxes
|
|
110
|
+
// inside the window, which are player settings — a reset that silently
|
|
111
|
+
// reverted someone's audio choice would be a worse lie than not resetting.
|
|
112
|
+
// `seed` is ignored; the menu has no randomness to seed.
|
|
113
|
+
reset: () => this.closeSettings(),
|
|
114
|
+
// Same `name` as GameFlow — one game, one identity; `scene` says which
|
|
115
|
+
// flow answered. Everything here must stay JSON-serialisable.
|
|
116
|
+
snapshot: () => ({
|
|
117
|
+
scene: 'menu',
|
|
118
|
+
gameScene: String(this.config.gameScene ?? ''),
|
|
119
|
+
playButtonReady: this.playButtonReady,
|
|
120
|
+
settingsButtonReady: this.settingsButtonReady,
|
|
121
|
+
settingsVisible: this.isSettingsVisible(),
|
|
122
|
+
// Checkbox state inside the settings window, keyed by node id
|
|
123
|
+
// (`music-toggle` / `sfx-toggle` in the shipped prefab).
|
|
124
|
+
settingsToggles: this.readSettingsToggles(),
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onDetach(): void {
|
|
130
|
+
// Safe in any order: the disposer clears the global only while it still
|
|
131
|
+
// holds *this* provider, so the game scene's registration is never wiped.
|
|
132
|
+
this.disposeDebug?.();
|
|
133
|
+
this.disposeDebug = null;
|
|
134
|
+
// The scene's registry is cleared on stop anyway; this covers the other
|
|
135
|
+
// case — this script being detached while the scene keeps running.
|
|
136
|
+
for (const dispose of this.disposeCommands) {
|
|
137
|
+
dispose();
|
|
138
|
+
}
|
|
139
|
+
this.disposeCommands = [];
|
|
140
|
+
this.playButtonReady = false;
|
|
141
|
+
this.settingsButtonReady = false;
|
|
142
|
+
super.onDetach();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Intent: leave the menu and transition into the game scene. */
|
|
146
|
+
startGame(): void {
|
|
147
|
+
const gameScene = String(this.config.gameScene ?? '');
|
|
148
|
+
if (!gameScene) {
|
|
149
|
+
console.warn('[MenuFlow] No game scene configured.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
void this.scene?.changeScene(gameScene, { transition: 'fade', durationSec: 0.3 });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Intent: show the settings window over the menu. */
|
|
156
|
+
openSettings(): void {
|
|
157
|
+
this.setNodeVisible(String(this.config.settingsNode ?? ''), true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Intent: hide the settings window (its own CLOSE button does this too). */
|
|
161
|
+
closeSettings(): void {
|
|
162
|
+
this.setNodeVisible(String(this.config.settingsNode ?? ''), false);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Returns whether the button was found and wired (reported in the snapshot). */
|
|
166
|
+
private connectButton(query: string, handler: () => void): boolean {
|
|
167
|
+
if (!query) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const button = this.findNode(query);
|
|
171
|
+
if (!button) {
|
|
172
|
+
console.warn(`[MenuFlow] Button "${query}" not found.`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
button.connect('pressed', this, handler);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private isSettingsVisible(): boolean {
|
|
180
|
+
const node = this.findNode(String(this.config.settingsNode ?? ''));
|
|
181
|
+
return node?.visible === true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Every Checkbox2D under the settings window, by node id → checked. */
|
|
185
|
+
private readSettingsToggles(): Record<string, boolean> {
|
|
186
|
+
const toggles: Record<string, boolean> = {};
|
|
187
|
+
const root = this.findNode(String(this.config.settingsNode ?? ''));
|
|
188
|
+
if (!root) {
|
|
189
|
+
return toggles;
|
|
190
|
+
}
|
|
191
|
+
const visit = (node: NodeBase): void => {
|
|
192
|
+
if (node instanceof Checkbox2D) {
|
|
193
|
+
toggles[node.nodeId] = node.checked;
|
|
194
|
+
}
|
|
195
|
+
for (const child of node.children) {
|
|
196
|
+
// `children` also holds plain Three.js objects (visuals), not just nodes.
|
|
197
|
+
if (child instanceof NodeBase) {
|
|
198
|
+
visit(child);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
visit(root);
|
|
203
|
+
return toggles;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private setNodeVisible(query: string, visible: boolean): void {
|
|
207
|
+
if (!query) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const node = this.findNode(query);
|
|
211
|
+
if (node) {
|
|
212
|
+
node.visible = visible;
|
|
213
|
+
} else {
|
|
214
|
+
console.warn(`[MenuFlow] Node "${query}" not found.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|