@pix3/cli 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +754 -0
  3. package/dist/index.js +494 -0
  4. package/dist/runtime-types.json +1 -0
  5. package/dist/smoke/prebuilt/build-7MIXIMSZ.mjs +948 -0
  6. package/dist/smoke/prebuilt/chunk-J2SJVMDO.mjs +2 -0
  7. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +239 -0
  8. package/dist/smoke/prebuilt/chunk-P2CBRXPJ.mjs +4050 -0
  9. package/dist/smoke/prebuilt/runtime.mjs +2 -0
  10. package/dist/smoke/prebuilt/smoke-worker.mjs +3 -0
  11. package/dist/smoke/prebuilt/three.mjs +2 -0
  12. package/dist/smoke/prebuilt/tree-defaults.mjs +2 -0
  13. package/dist/validate/prebuilt/build-AUGKXER3.js +948 -0
  14. package/dist/validate/prebuilt/bundle-main.js +24 -0
  15. package/dist/validate/prebuilt/chunk-6ACT6FNV.js +4050 -0
  16. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +239 -0
  17. package/dist/validate/prebuilt/runtime.js +2 -0
  18. package/dist/validate/prebuilt/three.js +2 -0
  19. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -0
  20. package/kit/files/.claude/skills/pix3-nodes/reference.md +1331 -0
  21. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +235 -0
  22. package/kit/files/.claude/skills/pix3-scene-format/reference.md +176 -0
  23. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +281 -0
  24. package/kit/files/.claude/skills/pix3-scripts/reference.md +638 -0
  25. package/kit/files/.claude/skills/pix3-verify/SKILL.md +386 -0
  26. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -0
  27. package/kit/files/AGENTS.md +203 -0
  28. package/kit/files/CLAUDE.md +1 -0
  29. package/kit/kit.json +24 -0
  30. package/package.json +54 -0
  31. package/templates/empty-2d/files/README.md +20 -0
  32. package/templates/empty-2d/files/scenes/main.pix3scene +68 -0
  33. package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
  34. package/templates/empty-2d/template.yaml +7 -0
  35. package/templates/empty-3d/files/README.md +20 -0
  36. package/templates/empty-3d/files/scenes/main.pix3scene +108 -0
  37. package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
  38. package/templates/empty-3d/template.yaml +7 -0
  39. package/templates/idea-blank/files/README.md +22 -0
  40. package/templates/idea-blank/files/scenes/main.pix3scene +37 -0
  41. package/templates/idea-blank/template.yaml +10 -0
  42. package/templates/minigame-2d/files/README.md +44 -0
  43. package/templates/minigame-2d/files/design/tests/reachability.json +1 -0
  44. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -0
  45. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -0
  46. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -0
  47. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -0
  48. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -0
  49. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -0
  50. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -0
  51. package/templates/minigame-2d/template.yaml +10 -0
  52. package/templates/playable-2d/files/README.md +30 -0
  53. package/templates/playable-2d/files/design/recipe.md +83 -0
  54. package/templates/playable-2d/files/design/tests/reachability.json +4 -0
  55. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -0
  56. package/templates/playable-2d/files/scenes/main.pix3scene +111 -0
  57. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -0
  58. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -0
  59. package/templates/playable-2d/files/scripts/CtaButton.ts +55 -0
  60. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -0
  61. package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
  62. package/templates/playable-2d/template.yaml +9 -0
  63. package/templates/playable-3d/files/README.md +32 -0
  64. package/templates/playable-3d/files/design/recipe.md +127 -0
  65. package/templates/playable-3d/files/design/tests/reachability.json +4 -0
  66. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -0
  67. package/templates/playable-3d/files/scenes/main.pix3scene +230 -0
  68. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -0
  69. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -0
  70. package/templates/playable-3d/files/scripts/CtaButton.ts +55 -0
  71. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -0
  72. package/templates/playable-3d/files/scripts/GameRules.ts +290 -0
  73. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -0
  74. package/templates/playable-3d/template.yaml +9 -0
  75. package/templates/recipe-arena-2d/files/README.md +48 -0
  76. package/templates/recipe-arena-2d/files/design/recipe.md +127 -0
  77. package/templates/recipe-arena-2d/files/design/tests/reachability.json +4 -0
  78. package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +23 -0
  79. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -0
  80. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -0
  81. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -0
  82. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -0
  83. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -0
  84. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -0
  85. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -0
  86. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -0
  87. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -0
  88. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -0
  89. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -0
  90. package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
  91. package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
  92. package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
  93. package/templates/recipe-arena-2d/template.yaml +10 -0
  94. package/templates/recipe-blank-2d/files/README.md +61 -0
  95. package/templates/recipe-blank-2d/files/design/recipe.md +148 -0
  96. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -0
  97. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -0
  98. package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +23 -0
  99. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -0
  100. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -0
  101. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -0
  102. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -0
  103. package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
  104. package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
  105. package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
  106. package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
  107. package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
  108. package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
  109. package/templates/recipe-blank-2d/template.yaml +10 -0
  110. package/templates/recipe-bouncer-2d/files/README.md +81 -0
  111. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -0
  112. package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +4 -0
  113. package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +23 -0
  114. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -0
  115. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -0
  116. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -0
  117. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -0
  118. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -0
  119. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -0
  120. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -0
  121. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -0
  122. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -0
  123. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -0
  124. package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
  125. package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
  126. package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
  127. package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
  128. package/templates/recipe-bouncer-2d/template.yaml +10 -0
  129. package/templates/recipe-grid-3d/files/README.md +22 -0
  130. package/templates/recipe-grid-3d/files/design/recipe.md +110 -0
  131. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -0
  132. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -0
  133. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -0
  134. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -0
  135. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -0
  136. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -0
  137. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -0
  138. package/templates/recipe-grid-3d/template.yaml +10 -0
  139. package/templates/recipe-tapper-2d/files/README.md +39 -0
  140. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -0
  141. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -0
  142. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -0
  143. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -0
  144. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -0
  145. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -0
  146. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -0
  147. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -0
  148. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -0
  149. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -0
  150. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -0
  151. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -0
  152. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -0
  153. package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
  154. package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
  155. package/templates/recipe-tapper-2d/template.yaml +10 -0
@@ -0,0 +1,118 @@
1
+ # Pix3 Scene File (YAML)
2
+ # Minigame 2D — the MENU scene. This is the project's ENTRY / export scene
3
+ # (Project Settings → Default Export Scene Path): a full build and "Play Game (Entry Scene)"
4
+ # boot here so you can debug the whole flow. PLAY transitions to the game scene
5
+ # (main.pix3scene) via a fade; SETTINGS opens the settings-window prefab.
6
+ # Wired by scripts/MenuFlow.ts.
7
+ version: 1.0.0
8
+ metadata:
9
+ author: Pix3 Template
10
+ description: Menu scene — entry point; PLAY transitions to the game
11
+ root:
12
+ - id: menu-root
13
+ type: Group2D
14
+ name: Menu Root
15
+ properties:
16
+ width: 1920
17
+ height: 1080
18
+ layout:
19
+ enabled: true
20
+ horizontalAlign: stretch
21
+ verticalAlign: stretch
22
+ components:
23
+ - id: menu-flow
24
+ type: user:MenuFlow
25
+ enabled: true
26
+ config:
27
+ gameScene: res://scenes/main.pix3scene
28
+ settingsNode: settings-window
29
+ playButton: play-button
30
+ settingsButton: settings-button
31
+ children:
32
+ - id: menu-background
33
+ type: ColorRect2D
34
+ name: Menu Background
35
+ properties:
36
+ width: 1920
37
+ height: 1080
38
+ color: "#16213e"
39
+ transform:
40
+ position: [0, 0]
41
+ scale: [1, 1]
42
+ rotation: 0
43
+ layout:
44
+ enabled: true
45
+ horizontalAlign: stretch
46
+ verticalAlign: stretch
47
+ children: []
48
+ - id: menu-title
49
+ type: Label2D
50
+ name: Menu Title
51
+ properties:
52
+ label: "{{PROJECT_NAME}}"
53
+ labelFontSize: 96
54
+ labelColor: "#ffcf33"
55
+ labelAlign: center
56
+ transform:
57
+ position: [0, 250]
58
+ scale: [1, 1]
59
+ rotation: 0
60
+ layout:
61
+ enabled: true
62
+ horizontalAlign: center
63
+ verticalAlign: center
64
+ children: []
65
+ - id: play-button
66
+ type: Button2D
67
+ name: Play Button
68
+ properties:
69
+ width: 420
70
+ height: 120
71
+ label: "PLAY"
72
+ labelFontSize: 48
73
+ labelColor: "#1a1a2e"
74
+ backgroundColor: "#ffcf33"
75
+ hoverColor: "#ffe066"
76
+ pressedColor: "#e6b800"
77
+ buttonAction: play
78
+ transform:
79
+ position: [0, 0]
80
+ scale: [1, 1]
81
+ rotation: 0
82
+ layout:
83
+ enabled: true
84
+ horizontalAlign: center
85
+ verticalAlign: center
86
+ children: []
87
+ - id: settings-button
88
+ type: Button2D
89
+ name: Settings Button
90
+ properties:
91
+ width: 420
92
+ height: 120
93
+ label: "SETTINGS"
94
+ labelFontSize: 48
95
+ labelColor: "#ffffff"
96
+ backgroundColor: "#3a3a5e"
97
+ hoverColor: "#4a4a6e"
98
+ pressedColor: "#2a2a4e"
99
+ buttonAction: open-settings
100
+ transform:
101
+ position: [0, -170]
102
+ scale: [1, 1]
103
+ rotation: 0
104
+ layout:
105
+ enabled: true
106
+ horizontalAlign: center
107
+ verticalAlign: center
108
+ children: []
109
+ # Settings modal — authored in scenes/ui/settings-window.pix3scene so this
110
+ # scene opens on the MENU, not on the modal. `visible: false` hides it in
111
+ # the EDITOR only; `initiallyVisible` (in the overlay file) is what play
112
+ # mode reads, and user:MenuFlow opens it at run time on SETTINGS. Tick the
113
+ # eye to preview it over the menu.
114
+ - id: settings-window
115
+ name: Settings Window
116
+ instance: res://scenes/ui/settings-window.pix3scene
117
+ properties:
118
+ visible: false
@@ -0,0 +1,138 @@
1
+ # Pix3 Scene File (YAML)
2
+ # Minigame 2D — the SETTINGS modal: a centered panel with Music/SFX checkboxes
3
+ # (wired to the audio buses by user:SettingsWindow) and a CLOSE button. It lives
4
+ # in its own file under scenes/ui/ so the menu scene opens clean. Host scenes
5
+ # carry it as a one-line `instance:` reference marked `visible: false`, which
6
+ # hides it in the EDITOR only; `initiallyVisible: false` on `settings-root`
7
+ # below is what play mode reads, and `user:MenuFlow` shows it at run time.
8
+ #
9
+ # Edit it here, full-screen, in its own tab. To preview it composited over the
10
+ # menu, tick the eye on `settings-window` in menu.pix3scene — just do not commit
11
+ # that tick, or every iteration starts behind the settings modal again.
12
+ #
13
+ # Instance it from any scene:
14
+ # - id: settings-window
15
+ # instance: res://scenes/ui/settings-window.pix3scene
16
+ # properties:
17
+ # visible: false
18
+ version: 1.0.0
19
+ metadata:
20
+ author: Pix3 Template
21
+ description: Reusable settings window (music/SFX toggles)
22
+ root:
23
+ - id: settings-root
24
+ type: Group2D
25
+ name: Settings Window
26
+ properties:
27
+ width: 640
28
+ height: 520
29
+ initiallyVisible: false
30
+ layout:
31
+ enabled: true
32
+ horizontalAlign: center
33
+ verticalAlign: center
34
+ components:
35
+ - id: settings-behavior
36
+ type: user:SettingsWindow
37
+ enabled: true
38
+ config:
39
+ musicToggle: music-toggle
40
+ sfxToggle: sfx-toggle
41
+ closeButton: close-button
42
+ children:
43
+ - id: settings-panel
44
+ type: ColorRect2D
45
+ name: Panel
46
+ properties:
47
+ width: 640
48
+ height: 520
49
+ color: "#1a1a2e"
50
+ opacity: 0.97
51
+ transform:
52
+ position: [0, 0]
53
+ scale: [1, 1]
54
+ rotation: 0
55
+ children: []
56
+ - id: settings-title
57
+ type: Label2D
58
+ name: Title
59
+ properties:
60
+ label: "SETTINGS"
61
+ labelFontSize: 48
62
+ labelColor: "#ffcf33"
63
+ labelAlign: center
64
+ transform:
65
+ position: [0, 190]
66
+ scale: [1, 1]
67
+ rotation: 0
68
+ children: []
69
+ - id: music-label
70
+ type: Label2D
71
+ name: Music Label
72
+ properties:
73
+ label: "Music"
74
+ labelFontSize: 36
75
+ labelColor: "#ffffff"
76
+ labelAlign: left
77
+ transform:
78
+ position: [-120, 70]
79
+ scale: [1, 1]
80
+ rotation: 0
81
+ children: []
82
+ - id: music-toggle
83
+ type: Checkbox2D
84
+ name: Music Toggle
85
+ properties:
86
+ size: 44
87
+ checked: true
88
+ checkedColor: "#ffcf33"
89
+ uncheckedColor: "#555577"
90
+ transform:
91
+ position: [150, 70]
92
+ scale: [1, 1]
93
+ rotation: 0
94
+ children: []
95
+ - id: sfx-label
96
+ type: Label2D
97
+ name: SFX Label
98
+ properties:
99
+ label: "Sound FX"
100
+ labelFontSize: 36
101
+ labelColor: "#ffffff"
102
+ labelAlign: left
103
+ transform:
104
+ position: [-120, -10]
105
+ scale: [1, 1]
106
+ rotation: 0
107
+ children: []
108
+ - id: sfx-toggle
109
+ type: Checkbox2D
110
+ name: SFX Toggle
111
+ properties:
112
+ size: 44
113
+ checked: true
114
+ checkedColor: "#ffcf33"
115
+ uncheckedColor: "#555577"
116
+ transform:
117
+ position: [150, -10]
118
+ scale: [1, 1]
119
+ rotation: 0
120
+ children: []
121
+ - id: close-button
122
+ type: Button2D
123
+ name: Close Button
124
+ properties:
125
+ width: 280
126
+ height: 88
127
+ label: "CLOSE"
128
+ labelFontSize: 36
129
+ labelColor: "#1a1a2e"
130
+ backgroundColor: "#ffcf33"
131
+ hoverColor: "#ffe066"
132
+ pressedColor: "#e6b800"
133
+ buttonAction: close-settings
134
+ transform:
135
+ position: [0, -180]
136
+ scale: [1, 1]
137
+ rotation: 0
138
+ children: []
@@ -0,0 +1,133 @@
1
+ /**
2
+ * GameFlow — drives the game scene (main.pix3scene).
3
+ *
4
+ * Attach to the game scene root. Wires the MENU button to transition back to the
5
+ * menu scene via a fade. Extend this with your win/lose flow — call
6
+ * `this.scene.changeScene('res://scenes/menu.pix3scene')` on game over
7
+ * to return to the menu, or transition to a results scene of your own.
8
+ *
9
+ * Two conventions in here are worth keeping as you build on this template:
10
+ *
11
+ * 1. **Intent-first handlers.** Every reaction to the UI lives in a named public
12
+ * method marked `Intent:`, registered as a **command** (`scene.commands`)
13
+ * that the signal handler dispatches. Tools (and you, from the console) then
14
+ * trigger the behaviour with `scene.commands.dispatch('return-to-menu')`
15
+ * instead of faking a click, and every raised intent is journalled with the
16
+ * frame it happened on.
17
+ * 2. **A debug provider.** `registerGameDebug` publishes a JSON snapshot of the
18
+ * game's state so agents and dev tooling can read what the game is doing
19
+ * instead of guessing from pixels. Add YOUR state to `snapshot()` — score,
20
+ * lives, level, whatever the game owns — and keep every value JSON-safe.
21
+ */
22
+ import { Script, registerGameDebug, type PropertySchema } from '@pix3/runtime';
23
+
24
+ /** Coarse state of the game scene; extend with your own phases (`won`, `lost`, …). */
25
+ type GamePhase = 'idle' | 'playing' | 'leaving';
26
+
27
+ export class GameFlow extends Script {
28
+ private phase: GamePhase = 'idle';
29
+ private menuButtonReady = false;
30
+ private disposeDebug: (() => void) | null = null;
31
+ private disposeCommands: (() => void)[] = [];
32
+
33
+ constructor(id: string, type: string) {
34
+ super(id, type);
35
+ this.config = {
36
+ // res:// path of the scene the MENU button transitions to.
37
+ menuScene: 'res://scenes/menu.pix3scene',
38
+ // Node id/name of the button that returns to the menu.
39
+ menuButton: 'menu-button',
40
+ };
41
+ }
42
+
43
+ static getPropertySchema(): PropertySchema {
44
+ const stringProp = (name: string, label: string) => ({
45
+ name,
46
+ type: 'string' as const,
47
+ ui: { label, group: 'Game' },
48
+ getValue: (s: unknown) => (s as GameFlow).config[name],
49
+ setValue: (s: unknown, v: unknown) => {
50
+ (s as GameFlow).config[name] = typeof v === 'string' ? v : '';
51
+ },
52
+ });
53
+
54
+ return {
55
+ nodeType: 'GameFlow',
56
+ properties: [stringProp('menuScene', 'Menu Scene'), stringProp('menuButton', 'Menu Button')],
57
+ groups: { Game: { label: 'Game Flow', expanded: true } },
58
+ };
59
+ }
60
+
61
+ onStart(): void {
62
+ this.phase = 'playing';
63
+ const commands = this.scene?.commands;
64
+ this.disposeCommands = [
65
+ commands?.register('return-to-menu', () => this.returnToMenu(), {
66
+ description: 'Leave the game and transition back to the menu scene.',
67
+ }),
68
+ ].filter((dispose): dispose is () => void => dispose !== undefined);
69
+
70
+ this.disposeDebug = registerGameDebug({
71
+ name: 'minigame-2d',
72
+ version: 1,
73
+ // The registry IS the action list — never a second, hand-kept copy.
74
+ actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
75
+ // What "the start" means for this scene, so a test can put the game back
76
+ // without restarting it. All this template owns is `phase`, and `playing`
77
+ // is the value `onStart` sets — a run that had already dispatched
78
+ // `return-to-menu` would otherwise be stuck in `leaving`. Reset YOUR state
79
+ // here as you add it (score, level, spawned things), and seed your RNG from
80
+ // `seed` if you want two runs to be comparable and not merely to start
81
+ // from the same values.
82
+ reset: () => {
83
+ this.phase = 'playing';
84
+ },
85
+ // Everything returned here must be JSON-serialisable — plain values only,
86
+ // never live nodes or Three.js objects. Add your own state as you build.
87
+ snapshot: () => ({
88
+ phase: this.phase,
89
+ menuScene: String(this.config.menuScene ?? ''),
90
+ menuButtonReady: this.menuButtonReady,
91
+ }),
92
+ });
93
+
94
+ const query = String(this.config.menuButton ?? '');
95
+ if (!query) {
96
+ return;
97
+ }
98
+ const button = this.findNode(query);
99
+ if (button) {
100
+ this.menuButtonReady = true;
101
+ // The button raises the intent through the registry, so a single tap in a
102
+ // test proves this binding and every later scenario can skip the tap.
103
+ button.connect('pressed', this, () => {
104
+ this.scene?.commands.dispatch('return-to-menu');
105
+ });
106
+ } else {
107
+ console.warn(`[GameFlow] Button "${query}" not found.`);
108
+ }
109
+ }
110
+
111
+ onDetach(): void {
112
+ this.disposeDebug?.();
113
+ this.disposeDebug = null;
114
+ for (const dispose of this.disposeCommands) {
115
+ dispose();
116
+ }
117
+ this.disposeCommands = [];
118
+ this.phase = 'idle';
119
+ this.menuButtonReady = false;
120
+ super.onDetach();
121
+ }
122
+
123
+ /** Intent: leave the game and transition back to the menu scene (also on game over). */
124
+ returnToMenu(): void {
125
+ const menuScene = String(this.config.menuScene ?? '');
126
+ if (!menuScene) {
127
+ console.warn('[GameFlow] No menu scene configured.');
128
+ return;
129
+ }
130
+ this.phase = 'leaving';
131
+ void this.scene?.changeScene(menuScene, { transition: 'fade', durationSec: 0.3 });
132
+ }
133
+ }
@@ -0,0 +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
+ }