@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,198 +1,198 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SettingsWindow — behavior for the settings-window prefab.
|
|
3
|
-
*
|
|
4
|
-
* Attach to the prefab root. The Music / SFX checkboxes mute or restore the
|
|
5
|
-
* engine audio buses (`scene.audio.setBusVolume`), CLOSE hides the window.
|
|
6
|
-
* Node lookups are relative to the prefab subtree so multiple instances work.
|
|
7
|
-
*
|
|
8
|
-
* Handlers are **intent-first**: each behaviour lives in a named public method
|
|
9
|
-
* marked `Intent:`, and the signal handlers below do nothing but call one. The
|
|
10
|
-
* two switches are also registered as commands — `settings.toggle-music` /
|
|
11
|
-
* `settings.toggle-sfx`, namespaced so this prefab's intents never collide with
|
|
12
|
-
* the menu's `open-settings` / `close-settings` — so a test flips sound without
|
|
13
|
-
* hunting for a checkbox on screen.
|
|
14
|
-
*
|
|
15
|
-
* Note on the checkboxes: they are event-driven, not polled. `Checkbox2D` splits
|
|
16
|
-
* its signals the way Godot does — `click` is the POINTER signal ("the box was
|
|
17
|
-
* clicked"), `toggled` is the STATE signal ("the box is now checked/unchecked"),
|
|
18
|
-
* emitted after the flip with the new value as its payload. Anything that
|
|
19
|
-
* applies the state connects to `toggled`, so the handler can simply read
|
|
20
|
-
* `checked` (or take the payload) and get the value the user just chose.
|
|
21
|
-
*/
|
|
22
|
-
import { Script, Checkbox2D, type NodeBase, type PropertySchema } from '@pix3/runtime';
|
|
23
|
-
|
|
24
|
-
/** One live signal connection, kept so `onDetach` can take it back down. */
|
|
25
|
-
interface SignalBinding {
|
|
26
|
-
node: NodeBase;
|
|
27
|
-
signal: string;
|
|
28
|
-
handler: () => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class SettingsWindow extends Script {
|
|
32
|
-
private musicToggle: Checkbox2D | null = null;
|
|
33
|
-
private sfxToggle: Checkbox2D | null = null;
|
|
34
|
-
private readonly bindings: SignalBinding[] = [];
|
|
35
|
-
private disposeCommands: (() => void)[] = [];
|
|
36
|
-
|
|
37
|
-
constructor(id: string, type: string) {
|
|
38
|
-
super(id, type);
|
|
39
|
-
this.config = {
|
|
40
|
-
musicToggle: 'music-toggle',
|
|
41
|
-
sfxToggle: 'sfx-toggle',
|
|
42
|
-
closeButton: 'close-button',
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
static getPropertySchema(): PropertySchema {
|
|
47
|
-
return {
|
|
48
|
-
nodeType: 'SettingsWindow',
|
|
49
|
-
properties: [
|
|
50
|
-
{
|
|
51
|
-
name: 'musicToggle',
|
|
52
|
-
type: 'string',
|
|
53
|
-
ui: { label: 'Music Checkbox', group: 'Settings' },
|
|
54
|
-
getValue: s => (s as SettingsWindow).config.musicToggle,
|
|
55
|
-
setValue: (s, v) => {
|
|
56
|
-
(s as SettingsWindow).config.musicToggle = typeof v === 'string' ? v : '';
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: 'sfxToggle',
|
|
61
|
-
type: 'string',
|
|
62
|
-
ui: { label: 'SFX Checkbox', group: 'Settings' },
|
|
63
|
-
getValue: s => (s as SettingsWindow).config.sfxToggle,
|
|
64
|
-
setValue: (s, v) => {
|
|
65
|
-
(s as SettingsWindow).config.sfxToggle = typeof v === 'string' ? v : '';
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'closeButton',
|
|
70
|
-
type: 'string',
|
|
71
|
-
ui: { label: 'Close Button', group: 'Settings' },
|
|
72
|
-
getValue: s => (s as SettingsWindow).config.closeButton,
|
|
73
|
-
setValue: (s, v) => {
|
|
74
|
-
(s as SettingsWindow).config.closeButton = typeof v === 'string' ? v : '';
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
groups: { Settings: { label: 'Settings Window', expanded: true } },
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
onStart(): void {
|
|
83
|
-
this.musicToggle = this.findInSubtree(String(this.config.musicToggle ?? ''));
|
|
84
|
-
this.sfxToggle = this.findInSubtree(String(this.config.sfxToggle ?? ''));
|
|
85
|
-
|
|
86
|
-
// Match the buses to the boxes as authored, before anyone clicks anything:
|
|
87
|
-
// without the old per-frame read, nothing else would ever apply the initial state.
|
|
88
|
-
if (this.musicToggle) {
|
|
89
|
-
this.setMusicEnabled(this.musicToggle.checked);
|
|
90
|
-
}
|
|
91
|
-
if (this.sfxToggle) {
|
|
92
|
-
this.setSfxEnabled(this.sfxToggle.checked);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.connectToggle(this.musicToggle, enabled => this.setMusicEnabled(enabled));
|
|
96
|
-
this.connectToggle(this.sfxToggle, enabled => this.setSfxEnabled(enabled));
|
|
97
|
-
|
|
98
|
-
// The commands flip the BOX, not the bus: `toggled` then applies the bus, so
|
|
99
|
-
// a dispatched intent leaves exactly the state a tap would (checkmark, bus,
|
|
100
|
-
// snapshot). Applying the bus here instead would drift the two apart, and
|
|
101
|
-
// dispatching from the `toggled` handler would make the command dispatch
|
|
102
|
-
// itself — the registry caps that recursion, but the loop is the bug.
|
|
103
|
-
const commands = this.scene?.commands;
|
|
104
|
-
this.disposeCommands = [
|
|
105
|
-
commands?.register('settings.toggle-music', () => this.musicToggle?.toggle(), {
|
|
106
|
-
description: 'Flip the music checkbox, muting or restoring the music bus.',
|
|
107
|
-
}),
|
|
108
|
-
commands?.register('settings.toggle-sfx', () => this.sfxToggle?.toggle(), {
|
|
109
|
-
description: 'Flip the SFX checkbox, muting or restoring the sound-effects bus.',
|
|
110
|
-
}),
|
|
111
|
-
].filter((dispose): dispose is () => void => dispose !== undefined);
|
|
112
|
-
|
|
113
|
-
const closeButton = this.findInSubtree<NodeBase>(String(this.config.closeButton ?? ''));
|
|
114
|
-
if (closeButton) {
|
|
115
|
-
this.bind(closeButton, 'pressed', () => this.closeWindow());
|
|
116
|
-
} else {
|
|
117
|
-
console.warn('[SettingsWindow] Close button not found.');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
onDetach(): void {
|
|
122
|
-
for (const binding of this.bindings) {
|
|
123
|
-
binding.node.disconnect(binding.signal, this, binding.handler);
|
|
124
|
-
}
|
|
125
|
-
this.bindings.length = 0;
|
|
126
|
-
// A settings window can be freed while its scene keeps running, and a
|
|
127
|
-
// command pointing at a detached prefab is exactly the dead intent the
|
|
128
|
-
// registry's scene-scoped lifetime is meant to prevent.
|
|
129
|
-
for (const dispose of this.disposeCommands) {
|
|
130
|
-
dispose();
|
|
131
|
-
}
|
|
132
|
-
this.disposeCommands = [];
|
|
133
|
-
this.musicToggle = null;
|
|
134
|
-
this.sfxToggle = null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Intent: hide the settings window. */
|
|
138
|
-
closeWindow(): void {
|
|
139
|
-
if (this.node) {
|
|
140
|
-
this.node.visible = false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Intent: mute or restore the music bus. */
|
|
145
|
-
setMusicEnabled(enabled: boolean): void {
|
|
146
|
-
if (!this.scene) {
|
|
147
|
-
return; // No audio yet — the next click applies it.
|
|
148
|
-
}
|
|
149
|
-
this.scene.audio.setBusVolume('music', enabled ? 1 : 0);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Intent: mute or restore the sound-effects bus. */
|
|
153
|
-
setSfxEnabled(enabled: boolean): void {
|
|
154
|
-
if (!this.scene) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
this.scene.audio.setBusVolume('sfx', enabled ? 1 : 0);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Call `apply` with the checkbox's new state. `toggled` fires once the flip is
|
|
162
|
-
* applied, so `checked` is the value the user just chose.
|
|
163
|
-
*/
|
|
164
|
-
private connectToggle(toggle: Checkbox2D | null, apply: (enabled: boolean) => void): void {
|
|
165
|
-
if (!toggle) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
this.bind(toggle, 'toggled', () => apply(toggle.checked));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Connect a signal and remember it, so `onDetach` leaves no listener behind. */
|
|
172
|
-
private bind(node: NodeBase, signal: string, handler: () => void): void {
|
|
173
|
-
node.connect(signal, this, handler);
|
|
174
|
-
this.bindings.push({ node, signal, handler });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Search this prefab's subtree by node id or name (instance-safe). */
|
|
178
|
-
private findInSubtree<T extends NodeBase = NodeBase>(query: string): T | null {
|
|
179
|
-
if (!query || !this.node) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
const visit = (node: NodeBase): NodeBase | null => {
|
|
183
|
-
if (node.nodeId === query || node.name === query) {
|
|
184
|
-
return node;
|
|
185
|
-
}
|
|
186
|
-
for (const child of node.children) {
|
|
187
|
-
if (child instanceof Object && 'nodeId' in child) {
|
|
188
|
-
const found = visit(child as NodeBase);
|
|
189
|
-
if (found) {
|
|
190
|
-
return found;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
};
|
|
196
|
-
return visit(this.node) as T | null;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SettingsWindow — behavior for the settings-window prefab.
|
|
3
|
+
*
|
|
4
|
+
* Attach to the prefab root. The Music / SFX checkboxes mute or restore the
|
|
5
|
+
* engine audio buses (`scene.audio.setBusVolume`), CLOSE hides the window.
|
|
6
|
+
* Node lookups are relative to the prefab subtree so multiple instances work.
|
|
7
|
+
*
|
|
8
|
+
* Handlers are **intent-first**: each behaviour lives in a named public method
|
|
9
|
+
* marked `Intent:`, and the signal handlers below do nothing but call one. The
|
|
10
|
+
* two switches are also registered as commands — `settings.toggle-music` /
|
|
11
|
+
* `settings.toggle-sfx`, namespaced so this prefab's intents never collide with
|
|
12
|
+
* the menu's `open-settings` / `close-settings` — so a test flips sound without
|
|
13
|
+
* hunting for a checkbox on screen.
|
|
14
|
+
*
|
|
15
|
+
* Note on the checkboxes: they are event-driven, not polled. `Checkbox2D` splits
|
|
16
|
+
* its signals the way Godot does — `click` is the POINTER signal ("the box was
|
|
17
|
+
* clicked"), `toggled` is the STATE signal ("the box is now checked/unchecked"),
|
|
18
|
+
* emitted after the flip with the new value as its payload. Anything that
|
|
19
|
+
* applies the state connects to `toggled`, so the handler can simply read
|
|
20
|
+
* `checked` (or take the payload) and get the value the user just chose.
|
|
21
|
+
*/
|
|
22
|
+
import { Script, Checkbox2D, type NodeBase, type PropertySchema } from '@pix3/runtime';
|
|
23
|
+
|
|
24
|
+
/** One live signal connection, kept so `onDetach` can take it back down. */
|
|
25
|
+
interface SignalBinding {
|
|
26
|
+
node: NodeBase;
|
|
27
|
+
signal: string;
|
|
28
|
+
handler: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SettingsWindow extends Script {
|
|
32
|
+
private musicToggle: Checkbox2D | null = null;
|
|
33
|
+
private sfxToggle: Checkbox2D | null = null;
|
|
34
|
+
private readonly bindings: SignalBinding[] = [];
|
|
35
|
+
private disposeCommands: (() => void)[] = [];
|
|
36
|
+
|
|
37
|
+
constructor(id: string, type: string) {
|
|
38
|
+
super(id, type);
|
|
39
|
+
this.config = {
|
|
40
|
+
musicToggle: 'music-toggle',
|
|
41
|
+
sfxToggle: 'sfx-toggle',
|
|
42
|
+
closeButton: 'close-button',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static getPropertySchema(): PropertySchema {
|
|
47
|
+
return {
|
|
48
|
+
nodeType: 'SettingsWindow',
|
|
49
|
+
properties: [
|
|
50
|
+
{
|
|
51
|
+
name: 'musicToggle',
|
|
52
|
+
type: 'string',
|
|
53
|
+
ui: { label: 'Music Checkbox', group: 'Settings' },
|
|
54
|
+
getValue: s => (s as SettingsWindow).config.musicToggle,
|
|
55
|
+
setValue: (s, v) => {
|
|
56
|
+
(s as SettingsWindow).config.musicToggle = typeof v === 'string' ? v : '';
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'sfxToggle',
|
|
61
|
+
type: 'string',
|
|
62
|
+
ui: { label: 'SFX Checkbox', group: 'Settings' },
|
|
63
|
+
getValue: s => (s as SettingsWindow).config.sfxToggle,
|
|
64
|
+
setValue: (s, v) => {
|
|
65
|
+
(s as SettingsWindow).config.sfxToggle = typeof v === 'string' ? v : '';
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'closeButton',
|
|
70
|
+
type: 'string',
|
|
71
|
+
ui: { label: 'Close Button', group: 'Settings' },
|
|
72
|
+
getValue: s => (s as SettingsWindow).config.closeButton,
|
|
73
|
+
setValue: (s, v) => {
|
|
74
|
+
(s as SettingsWindow).config.closeButton = typeof v === 'string' ? v : '';
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
groups: { Settings: { label: 'Settings Window', expanded: true } },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onStart(): void {
|
|
83
|
+
this.musicToggle = this.findInSubtree(String(this.config.musicToggle ?? ''));
|
|
84
|
+
this.sfxToggle = this.findInSubtree(String(this.config.sfxToggle ?? ''));
|
|
85
|
+
|
|
86
|
+
// Match the buses to the boxes as authored, before anyone clicks anything:
|
|
87
|
+
// without the old per-frame read, nothing else would ever apply the initial state.
|
|
88
|
+
if (this.musicToggle) {
|
|
89
|
+
this.setMusicEnabled(this.musicToggle.checked);
|
|
90
|
+
}
|
|
91
|
+
if (this.sfxToggle) {
|
|
92
|
+
this.setSfxEnabled(this.sfxToggle.checked);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.connectToggle(this.musicToggle, enabled => this.setMusicEnabled(enabled));
|
|
96
|
+
this.connectToggle(this.sfxToggle, enabled => this.setSfxEnabled(enabled));
|
|
97
|
+
|
|
98
|
+
// The commands flip the BOX, not the bus: `toggled` then applies the bus, so
|
|
99
|
+
// a dispatched intent leaves exactly the state a tap would (checkmark, bus,
|
|
100
|
+
// snapshot). Applying the bus here instead would drift the two apart, and
|
|
101
|
+
// dispatching from the `toggled` handler would make the command dispatch
|
|
102
|
+
// itself — the registry caps that recursion, but the loop is the bug.
|
|
103
|
+
const commands = this.scene?.commands;
|
|
104
|
+
this.disposeCommands = [
|
|
105
|
+
commands?.register('settings.toggle-music', () => this.musicToggle?.toggle(), {
|
|
106
|
+
description: 'Flip the music checkbox, muting or restoring the music bus.',
|
|
107
|
+
}),
|
|
108
|
+
commands?.register('settings.toggle-sfx', () => this.sfxToggle?.toggle(), {
|
|
109
|
+
description: 'Flip the SFX checkbox, muting or restoring the sound-effects bus.',
|
|
110
|
+
}),
|
|
111
|
+
].filter((dispose): dispose is () => void => dispose !== undefined);
|
|
112
|
+
|
|
113
|
+
const closeButton = this.findInSubtree<NodeBase>(String(this.config.closeButton ?? ''));
|
|
114
|
+
if (closeButton) {
|
|
115
|
+
this.bind(closeButton, 'pressed', () => this.closeWindow());
|
|
116
|
+
} else {
|
|
117
|
+
console.warn('[SettingsWindow] Close button not found.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onDetach(): void {
|
|
122
|
+
for (const binding of this.bindings) {
|
|
123
|
+
binding.node.disconnect(binding.signal, this, binding.handler);
|
|
124
|
+
}
|
|
125
|
+
this.bindings.length = 0;
|
|
126
|
+
// A settings window can be freed while its scene keeps running, and a
|
|
127
|
+
// command pointing at a detached prefab is exactly the dead intent the
|
|
128
|
+
// registry's scene-scoped lifetime is meant to prevent.
|
|
129
|
+
for (const dispose of this.disposeCommands) {
|
|
130
|
+
dispose();
|
|
131
|
+
}
|
|
132
|
+
this.disposeCommands = [];
|
|
133
|
+
this.musicToggle = null;
|
|
134
|
+
this.sfxToggle = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Intent: hide the settings window. */
|
|
138
|
+
closeWindow(): void {
|
|
139
|
+
if (this.node) {
|
|
140
|
+
this.node.visible = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Intent: mute or restore the music bus. */
|
|
145
|
+
setMusicEnabled(enabled: boolean): void {
|
|
146
|
+
if (!this.scene) {
|
|
147
|
+
return; // No audio yet — the next click applies it.
|
|
148
|
+
}
|
|
149
|
+
this.scene.audio.setBusVolume('music', enabled ? 1 : 0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Intent: mute or restore the sound-effects bus. */
|
|
153
|
+
setSfxEnabled(enabled: boolean): void {
|
|
154
|
+
if (!this.scene) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.scene.audio.setBusVolume('sfx', enabled ? 1 : 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Call `apply` with the checkbox's new state. `toggled` fires once the flip is
|
|
162
|
+
* applied, so `checked` is the value the user just chose.
|
|
163
|
+
*/
|
|
164
|
+
private connectToggle(toggle: Checkbox2D | null, apply: (enabled: boolean) => void): void {
|
|
165
|
+
if (!toggle) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.bind(toggle, 'toggled', () => apply(toggle.checked));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Connect a signal and remember it, so `onDetach` leaves no listener behind. */
|
|
172
|
+
private bind(node: NodeBase, signal: string, handler: () => void): void {
|
|
173
|
+
node.connect(signal, this, handler);
|
|
174
|
+
this.bindings.push({ node, signal, handler });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Search this prefab's subtree by node id or name (instance-safe). */
|
|
178
|
+
private findInSubtree<T extends NodeBase = NodeBase>(query: string): T | null {
|
|
179
|
+
if (!query || !this.node) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const visit = (node: NodeBase): NodeBase | null => {
|
|
183
|
+
if (node.nodeId === query || node.name === query) {
|
|
184
|
+
return node;
|
|
185
|
+
}
|
|
186
|
+
for (const child of node.children) {
|
|
187
|
+
if (child instanceof Object && 'nodeId' in child) {
|
|
188
|
+
const found = visit(child as NodeBase);
|
|
189
|
+
if (found) {
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
};
|
|
196
|
+
return visit(this.node) as T | null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
id: minigame-2d
|
|
2
|
-
title: Minigame 2D
|
|
3
|
-
description: Separate menu + game scenes with a scene transition, and a reusable settings-window prefab (music/SFX toggles wired to audio buses).
|
|
4
|
-
projectType: 2d
|
|
5
|
-
targetPlatform: universal
|
|
6
|
-
viewport: { width: 1920, height: 1080 }
|
|
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: 50
|
|
1
|
+
id: minigame-2d
|
|
2
|
+
title: Minigame 2D
|
|
3
|
+
description: Separate menu + game scenes with a scene transition, and a reusable settings-window prefab (music/SFX toggles wired to audio buses).
|
|
4
|
+
projectType: 2d
|
|
5
|
+
targetPlatform: universal
|
|
6
|
+
viewport: { width: 1920, height: 1080 }
|
|
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: 50
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
# {{PROJECT_NAME}}
|
|
2
|
-
|
|
3
|
-
A Pix3 project created from the **Playable 2D** template — a portrait playable-ad blank.
|
|
4
|
-
|
|
5
|
-
## What's included
|
|
6
|
-
|
|
7
|
-
- **Tap-to-start intro** (`intro-overlay`, `scenes/ui/intro.pix3scene`) — the first tap unlocks browser audio and starts the game (`scripts/GameFlow.ts`).
|
|
8
|
-
- **Gameplay placeholder** — replace the animated logo sprite in `main.pix3scene` with your game.
|
|
9
|
-
- **End screen with CTA** (`end-screen`, `scenes/ui/end-screen.pix3scene`) — the PLAY NOW button reports game end (`playable.gameEnd()`) and logs the click. It opens nothing: the ad network decides the store target at delivery time (`mraid.open` / `dapi.openStoreUrl`), so wire its SDK in `CtaButton.ctaClick()` when you package the ad.
|
|
10
|
-
- `GameFlow` shows the end screen automatically after `autoWinAfterSec` seconds — replace that with your real win/lose condition.
|
|
11
|
-
- **Readable state** — `GameFlow` publishes a `registerGameDebug` snapshot (phase, timer, overlay visibility, session end), so tooling reads the run's state instead of judging it from a screenshot. Add your own fields as you add state.
|
|
12
|
-
- **Intent-first handlers** — every reaction to the player is a named method (`GameFlow.start/finish/restart`, `CtaButton.ctaClick`) that the event handler merely calls; keep that shape and the game stays drivable by intent.
|
|
13
|
-
|
|
14
|
-
## Project structure
|
|
15
|
-
|
|
16
|
-
- `design/` — game design document and reference images
|
|
17
|
-
- `design/tests/` — agent-testing material: `routines/*.json` (named step +
|
|
18
|
-
assertion scripts, written on the highest channel the game offers) and
|
|
19
|
-
`reachability.json` (controls proven reachable on a live run). The harness
|
|
20
|
-
that runs them is still in development; format and location are fixed now.
|
|
21
|
-
- `scenes/` — scene files; `main.pix3scene` is the startup scene
|
|
22
|
-
- `scenes/ui/` — the full-screen overlays (`intro.pix3scene`, `end-screen.pix3scene`),
|
|
23
|
-
instanced into `main.pix3scene` with `visible: false` so the editor opens on the
|
|
24
|
-
game. That flag is editor-only; `initiallyVisible` on each overlay's root is what
|
|
25
|
-
play mode reads. Edit an overlay in its own tab, or tick the eye on its instance
|
|
26
|
-
node to preview it composited — just don't save that tick.
|
|
27
|
-
- `sprites/` — images and textures
|
|
28
|
-
- `scripts/` — game scripts (`export class X extends Script`, used as `user:X`)
|
|
29
|
-
- `audio/` — music and sound effects
|
|
30
|
-
- `AGENTS.md` — rules and pointers for AI agents working on this project
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A Pix3 project created from the **Playable 2D** template — a portrait playable-ad blank.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
- **Tap-to-start intro** (`intro-overlay`, `scenes/ui/intro.pix3scene`) — the first tap unlocks browser audio and starts the game (`scripts/GameFlow.ts`).
|
|
8
|
+
- **Gameplay placeholder** — replace the animated logo sprite in `main.pix3scene` with your game.
|
|
9
|
+
- **End screen with CTA** (`end-screen`, `scenes/ui/end-screen.pix3scene`) — the PLAY NOW button reports game end (`playable.gameEnd()`) and logs the click. It opens nothing: the ad network decides the store target at delivery time (`mraid.open` / `dapi.openStoreUrl`), so wire its SDK in `CtaButton.ctaClick()` when you package the ad.
|
|
10
|
+
- `GameFlow` shows the end screen automatically after `autoWinAfterSec` seconds — replace that with your real win/lose condition.
|
|
11
|
+
- **Readable state** — `GameFlow` publishes a `registerGameDebug` snapshot (phase, timer, overlay visibility, session end), so tooling reads the run's state instead of judging it from a screenshot. Add your own fields as you add state.
|
|
12
|
+
- **Intent-first handlers** — every reaction to the player is a named method (`GameFlow.start/finish/restart`, `CtaButton.ctaClick`) that the event handler merely calls; keep that shape and the game stays drivable by intent.
|
|
13
|
+
|
|
14
|
+
## Project structure
|
|
15
|
+
|
|
16
|
+
- `design/` — game design document and reference images
|
|
17
|
+
- `design/tests/` — agent-testing material: `routines/*.json` (named step +
|
|
18
|
+
assertion scripts, written on the highest channel the game offers) and
|
|
19
|
+
`reachability.json` (controls proven reachable on a live run). The harness
|
|
20
|
+
that runs them is still in development; format and location are fixed now.
|
|
21
|
+
- `scenes/` — scene files; `main.pix3scene` is the startup scene
|
|
22
|
+
- `scenes/ui/` — the full-screen overlays (`intro.pix3scene`, `end-screen.pix3scene`),
|
|
23
|
+
instanced into `main.pix3scene` with `visible: false` so the editor opens on the
|
|
24
|
+
game. That flag is editor-only; `initiallyVisible` on each overlay's root is what
|
|
25
|
+
play mode reads. Edit an overlay in its own tab, or tick the eye on its instance
|
|
26
|
+
node to preview it composited — just don't save that tick.
|
|
27
|
+
- `sprites/` — images and textures
|
|
28
|
+
- `scripts/` — game scripts (`export class X extends Script`, used as `user:X`)
|
|
29
|
+
- `audio/` — music and sound effects
|
|
30
|
+
- `AGENTS.md` — rules and pointers for AI agents working on this project
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
# Recipe: recipe-playable-ad
|
|
2
|
-
|
|
3
|
-
## What this is
|
|
4
|
-
|
|
5
|
-
The playable-ad shape: a portrait scene with a tap-to-start gate, a short piece
|
|
6
|
-
of gameplay, and an end screen whose CTA button reports game end through the
|
|
7
|
-
engine Playable SDK and logs the click. It opens no store page — the ad network
|
|
8
|
-
picks that target at delivery time, so hook its SDK into `CtaButton.ctaClick()`
|
|
9
|
-
when you package the ad. One scene, no menu — an ad has no menu.
|
|
10
|
-
|
|
11
|
-
The gameplay itself is deliberately a placeholder (`hero-sprite` bobbing on a
|
|
12
|
-
`core:Sine`) plus an auto-win timer. Replace both: keep the gate and the CTA,
|
|
13
|
-
build the middle. If the brief needs real mechanics, start from `recipe-arena-2d`,
|
|
14
|
-
`recipe-bouncer-2d` or `recipe-tapper-2d` and port this gate/CTA structure over.
|
|
15
|
-
|
|
16
|
-
## Node map
|
|
17
|
-
|
|
18
|
-
| id | what |
|
|
19
|
-
| --- | --- |
|
|
20
|
-
| `game-root` | scene root; hosts `GameFlow` (the phase driver: intro → playing → ended) |
|
|
21
|
-
| `background` | full-screen `ColorRect2D` (palette background) |
|
|
22
|
-
| `hero-sprite` | placeholder gameplay object, bobbing on a `core:Sine` |
|
|
23
|
-
| `hud-label` | in-game text line |
|
|
24
|
-
| `intro-overlay` → `intro-dim`, `intro-label` | the tap gate (`scenes/ui/intro.pix3scene`, instanced into main); the first tap hides it, starts the game, and unlocks browser audio |
|
|
25
|
-
| `end-screen` → `end-dim`, `end-label`, `cta-button` | end screen (`scenes/ui/end-screen.pix3scene`, instanced into main), `initiallyVisible: false` |
|
|
26
|
-
| `cta-button` | hosts `CtaButton` — `playable.gameEnd()` + a `[CtaButton] CTA clicked` log; the store call belongs to the ad network SDK, not to the template |
|
|
27
|
-
|
|
28
|
-
`GameFlow.finish()` reveals the end screen; call it from your gameplay code for
|
|
29
|
-
a real win/lose instead of the placeholder timer.
|
|
30
|
-
|
|
31
|
-
## Placeholders
|
|
32
|
-
|
|
33
|
-
| role | file | node |
|
|
34
|
-
| --- | --- | --- |
|
|
35
|
-
| hero | `sprites/pix3-logo.png` | `hero-sprite` |
|
|
36
|
-
|
|
37
|
-
## Tunables
|
|
38
|
-
|
|
39
|
-
```yaml
|
|
40
|
-
tunables:
|
|
41
|
-
autoWinAfterSec: { node: game-root, component: "user:GameFlow", property: autoWinAfterSec, min: 0, max: 120, default: 15 }
|
|
42
|
-
introNode: { node: game-root, component: "user:GameFlow", property: introNode, default: intro-overlay }
|
|
43
|
-
endNode: { node: game-root, component: "user:GameFlow", property: endNode, default: end-screen }
|
|
44
|
-
skipIntro: { node: game-root, component: "user:GameFlow", property: skipIntro, default: false }
|
|
45
|
-
bgColor: { node: background, property: color, default: "#16213e" }
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`component` present → `set_component_property`; absent → `set_property`.
|
|
49
|
-
|
|
50
|
-
`skipIntro: true` starts the run already playing, with the tap gate hidden — a
|
|
51
|
-
prototyping vent so iterating does not cost a tap per reload. Turn it back off
|
|
52
|
-
before shipping: in an ad container that first tap is what unlocks browser audio.
|
|
53
|
-
|
|
54
|
-
## Extension points
|
|
55
|
-
|
|
56
|
-
- **Real gameplay.** Replace `hero-sprite` and set `autoWinAfterSec: 0`, then
|
|
57
|
-
call `finish()` on the `GameFlow` component from your own script when the
|
|
58
|
-
player wins or loses. Everything else (gate, end screen, CTA) stays.
|
|
59
|
-
- **Fail state.** Add a `lose-label` inside `end-screen` and pick the text in
|
|
60
|
-
your `finish()` caller — a playable that can only be won reads as a demo.
|
|
61
|
-
- **Second CTA.** Any `Button2D` with a `user:CtaButton` component works, so an
|
|
62
|
-
early "install now" banner is one node.
|
|
63
|
-
- **Network requirements.** Keep everything self-contained: an ad bundle is a
|
|
64
|
-
single HTML file, so no external fetches, and prefer few, small textures.
|
|
65
|
-
|
|
66
|
-
## Do not touch
|
|
67
|
-
|
|
68
|
-
- The node ids above (rename `name`, never `id`).
|
|
69
|
-
- `intro-overlay` must exist and be visible at start (`initiallyVisible: true` in
|
|
70
|
-
`scenes/ui/intro.pix3scene`): the first tap is what unlocks browser audio, so a
|
|
71
|
-
playable without a gate ships silent.
|
|
72
|
-
- `end-screen` must keep `initiallyVisible: false` (in `scenes/ui/end-screen.pix3scene`).
|
|
73
|
-
- The `visible: false` on both `instance:` nodes in `main.pix3scene` is an
|
|
74
|
-
editor-only hide so the scene opens on the game — keep it, and author
|
|
75
|
-
`initiallyVisible` in the overlay file, never as an instance override.
|
|
76
|
-
- Do not import rapier — it would put a ~2 MB wasm payload in an ad bundle.
|
|
77
|
-
|
|
78
|
-
## Verify
|
|
79
|
-
|
|
80
|
-
1. `play_start` on `scenes/main.pix3scene`; `intro-overlay` is visible.
|
|
81
|
-
2. `game_input` a tap → `intro-overlay` hides and `hero-sprite` starts bobbing.
|
|
82
|
-
3. Wait `autoWinAfterSec` → `end-screen` becomes visible with `cta-button`.
|
|
83
|
-
4. Press `cta-button` → the SDK reports game end and `[CtaButton] CTA clicked` appears in the console (nothing navigates away).
|
|
1
|
+
# Recipe: recipe-playable-ad
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
The playable-ad shape: a portrait scene with a tap-to-start gate, a short piece
|
|
6
|
+
of gameplay, and an end screen whose CTA button reports game end through the
|
|
7
|
+
engine Playable SDK and logs the click. It opens no store page — the ad network
|
|
8
|
+
picks that target at delivery time, so hook its SDK into `CtaButton.ctaClick()`
|
|
9
|
+
when you package the ad. One scene, no menu — an ad has no menu.
|
|
10
|
+
|
|
11
|
+
The gameplay itself is deliberately a placeholder (`hero-sprite` bobbing on a
|
|
12
|
+
`core:Sine`) plus an auto-win timer. Replace both: keep the gate and the CTA,
|
|
13
|
+
build the middle. If the brief needs real mechanics, start from `recipe-arena-2d`,
|
|
14
|
+
`recipe-bouncer-2d` or `recipe-tapper-2d` and port this gate/CTA structure over.
|
|
15
|
+
|
|
16
|
+
## Node map
|
|
17
|
+
|
|
18
|
+
| id | what |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `game-root` | scene root; hosts `GameFlow` (the phase driver: intro → playing → ended) |
|
|
21
|
+
| `background` | full-screen `ColorRect2D` (palette background) |
|
|
22
|
+
| `hero-sprite` | placeholder gameplay object, bobbing on a `core:Sine` |
|
|
23
|
+
| `hud-label` | in-game text line |
|
|
24
|
+
| `intro-overlay` → `intro-dim`, `intro-label` | the tap gate (`scenes/ui/intro.pix3scene`, instanced into main); the first tap hides it, starts the game, and unlocks browser audio |
|
|
25
|
+
| `end-screen` → `end-dim`, `end-label`, `cta-button` | end screen (`scenes/ui/end-screen.pix3scene`, instanced into main), `initiallyVisible: false` |
|
|
26
|
+
| `cta-button` | hosts `CtaButton` — `playable.gameEnd()` + a `[CtaButton] CTA clicked` log; the store call belongs to the ad network SDK, not to the template |
|
|
27
|
+
|
|
28
|
+
`GameFlow.finish()` reveals the end screen; call it from your gameplay code for
|
|
29
|
+
a real win/lose instead of the placeholder timer.
|
|
30
|
+
|
|
31
|
+
## Placeholders
|
|
32
|
+
|
|
33
|
+
| role | file | node |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| hero | `sprites/pix3-logo.png` | `hero-sprite` |
|
|
36
|
+
|
|
37
|
+
## Tunables
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
tunables:
|
|
41
|
+
autoWinAfterSec: { node: game-root, component: "user:GameFlow", property: autoWinAfterSec, min: 0, max: 120, default: 15 }
|
|
42
|
+
introNode: { node: game-root, component: "user:GameFlow", property: introNode, default: intro-overlay }
|
|
43
|
+
endNode: { node: game-root, component: "user:GameFlow", property: endNode, default: end-screen }
|
|
44
|
+
skipIntro: { node: game-root, component: "user:GameFlow", property: skipIntro, default: false }
|
|
45
|
+
bgColor: { node: background, property: color, default: "#16213e" }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`component` present → `set_component_property`; absent → `set_property`.
|
|
49
|
+
|
|
50
|
+
`skipIntro: true` starts the run already playing, with the tap gate hidden — a
|
|
51
|
+
prototyping vent so iterating does not cost a tap per reload. Turn it back off
|
|
52
|
+
before shipping: in an ad container that first tap is what unlocks browser audio.
|
|
53
|
+
|
|
54
|
+
## Extension points
|
|
55
|
+
|
|
56
|
+
- **Real gameplay.** Replace `hero-sprite` and set `autoWinAfterSec: 0`, then
|
|
57
|
+
call `finish()` on the `GameFlow` component from your own script when the
|
|
58
|
+
player wins or loses. Everything else (gate, end screen, CTA) stays.
|
|
59
|
+
- **Fail state.** Add a `lose-label` inside `end-screen` and pick the text in
|
|
60
|
+
your `finish()` caller — a playable that can only be won reads as a demo.
|
|
61
|
+
- **Second CTA.** Any `Button2D` with a `user:CtaButton` component works, so an
|
|
62
|
+
early "install now" banner is one node.
|
|
63
|
+
- **Network requirements.** Keep everything self-contained: an ad bundle is a
|
|
64
|
+
single HTML file, so no external fetches, and prefer few, small textures.
|
|
65
|
+
|
|
66
|
+
## Do not touch
|
|
67
|
+
|
|
68
|
+
- The node ids above (rename `name`, never `id`).
|
|
69
|
+
- `intro-overlay` must exist and be visible at start (`initiallyVisible: true` in
|
|
70
|
+
`scenes/ui/intro.pix3scene`): the first tap is what unlocks browser audio, so a
|
|
71
|
+
playable without a gate ships silent.
|
|
72
|
+
- `end-screen` must keep `initiallyVisible: false` (in `scenes/ui/end-screen.pix3scene`).
|
|
73
|
+
- The `visible: false` on both `instance:` nodes in `main.pix3scene` is an
|
|
74
|
+
editor-only hide so the scene opens on the game — keep it, and author
|
|
75
|
+
`initiallyVisible` in the overlay file, never as an instance override.
|
|
76
|
+
- Do not import rapier — it would put a ~2 MB wasm payload in an ad bundle.
|
|
77
|
+
|
|
78
|
+
## Verify
|
|
79
|
+
|
|
80
|
+
1. `play_start` on `scenes/main.pix3scene`; `intro-overlay` is visible.
|
|
81
|
+
2. `game_input` a tap → `intro-overlay` hides and `hero-sprite` starts bobbing.
|
|
82
|
+
3. Wait `autoWinAfterSec` → `end-screen` becomes visible with `cta-button`.
|
|
83
|
+
4. Press `cta-button` → the SDK reports game end and `[CtaButton] CTA clicked` appears in the console (nothing navigates away).
|