@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.
Files changed (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-7MZGPAUK.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-7WOFGLW3.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-HKG6YI6Y.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +32 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. 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).