@pix3/cli 1.6.3 → 2.0.0-alpha.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 (168) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +722 -788
  3. package/dist/index.js +557 -421
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
  6. package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
  7. package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
  8. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  9. package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
  10. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  11. package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
  12. package/dist/validate/prebuilt/bundle-main.js +223 -18
  13. package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
  14. package/dist/validate/prebuilt/runtime.js +1 -1
  15. package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
  16. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
  17. package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
  18. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
  19. package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
  20. package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
  21. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
  22. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
  23. package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
  24. package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
  25. package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
  26. package/kit/files/AGENTS.md +76 -84
  27. package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
  28. package/kit/kit.json +8 -5
  29. package/package.json +62 -54
  30. package/templates/2d/files/scenes/main.pix3scene +31 -0
  31. package/templates/2d/template.yaml +11 -0
  32. package/templates/3d/files/scenes/main.pix3scene +50 -0
  33. package/templates/3d/template.yaml +11 -0
  34. package/templates/base/files/README.md +39 -0
  35. package/templates/base/files/gitignore +11 -0
  36. package/templates/base/files/index.html +23 -0
  37. package/templates/base/files/package.json +28 -0
  38. package/templates/base/files/src/main.ts +3 -0
  39. package/templates/base/files/tsconfig.json +17 -0
  40. package/templates/base/files/vite.config.ts +10 -0
  41. package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
  42. package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
  43. package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
  44. package/templates/empty-2d/files/README.md +0 -20
  45. package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
  46. package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
  47. package/templates/empty-2d/template.yaml +0 -7
  48. package/templates/empty-3d/files/README.md +0 -20
  49. package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
  50. package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
  51. package/templates/empty-3d/template.yaml +0 -7
  52. package/templates/idea-blank/files/README.md +0 -22
  53. package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
  54. package/templates/idea-blank/template.yaml +0 -10
  55. package/templates/minigame-2d/files/README.md +0 -44
  56. package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
  57. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
  58. package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
  59. package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
  60. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
  61. package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
  62. package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
  63. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
  64. package/templates/minigame-2d/template.yaml +0 -10
  65. package/templates/playable-2d/files/README.md +0 -30
  66. package/templates/playable-2d/files/design/recipe.md +0 -83
  67. package/templates/playable-2d/files/design/tests/reachability.json +0 -4
  68. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
  69. package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
  70. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
  71. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
  72. package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
  73. package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
  74. package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
  75. package/templates/playable-2d/template.yaml +0 -9
  76. package/templates/playable-3d/files/README.md +0 -32
  77. package/templates/playable-3d/files/design/recipe.md +0 -127
  78. package/templates/playable-3d/files/design/tests/reachability.json +0 -4
  79. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
  80. package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
  81. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
  82. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
  83. package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
  84. package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
  85. package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
  86. package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
  87. package/templates/playable-3d/template.yaml +0 -9
  88. package/templates/recipe-arena-2d/files/README.md +0 -48
  89. package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
  90. package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
  91. package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
  92. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
  93. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
  94. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
  95. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
  96. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
  97. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
  98. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
  99. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
  100. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
  101. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
  102. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
  103. package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
  104. package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
  105. package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
  106. package/templates/recipe-arena-2d/template.yaml +0 -10
  107. package/templates/recipe-blank-2d/files/README.md +0 -61
  108. package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
  109. package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
  110. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
  111. package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
  112. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
  113. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
  114. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
  115. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
  116. package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
  117. package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
  118. package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
  119. package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
  120. package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
  121. package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
  122. package/templates/recipe-blank-2d/template.yaml +0 -10
  123. package/templates/recipe-bouncer-2d/files/README.md +0 -81
  124. package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
  125. package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
  126. package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
  127. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
  128. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
  129. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
  130. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
  131. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
  132. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
  133. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
  134. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
  135. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
  136. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
  137. package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
  138. package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
  139. package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
  140. package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
  141. package/templates/recipe-bouncer-2d/template.yaml +0 -10
  142. package/templates/recipe-grid-3d/files/README.md +0 -22
  143. package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
  144. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
  145. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
  146. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
  147. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
  148. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
  149. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
  150. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
  151. package/templates/recipe-grid-3d/template.yaml +0 -10
  152. package/templates/recipe-tapper-2d/files/README.md +0 -39
  153. package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
  154. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
  155. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
  156. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
  157. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
  158. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
  159. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
  160. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
  161. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
  162. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
  163. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
  164. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
  165. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
  166. package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
  167. package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
  168. package/templates/recipe-tapper-2d/template.yaml +0 -10
@@ -1,256 +0,0 @@
1
- /**
2
- * GameFlow — the playable's phase driver: intro → playing → ended.
3
- *
4
- * Attach to `game-root`. While the intro overlay is visible, the first tap
5
- * hides it and starts the game (the tap also unlocks browser audio — the
6
- * engine's AudioService resumes its context on the first gesture). The end
7
- * screen is revealed either by calling `finish()` from game code or, as a
8
- * placeholder, after `autoWinAfterSec` seconds. Replace the timer with your
9
- * real win/lose condition.
10
- *
11
- * `skipIntro` starts the run already playing, gate hidden — a prototyping vent, so
12
- * that iterating on the game does not cost a tap per reload. Turn it back off before
13
- * shipping: in an ad container the first tap is what unlocks browser audio, so a
14
- * playable that never gates on one is a playable that plays silently.
15
- *
16
- * Two conventions here are worth keeping as you replace the placeholder:
17
- *
18
- * - **Intent-first handlers.** Every reaction to the player lives in a named
19
- * public method marked `Intent:` (`start`, `finish`, `restart`), and the ones
20
- * a test drives are registered as commands (`scene.commands`) that the event
21
- * handler dispatches. Game code and automated tests then drive the game by
22
- * intent instead of by simulated pixels — `scene.commands.dispatch('restart')`
23
- * — the flow survives any relayout of the UI, and every raised intent is
24
- * journalled with the frame it happened on.
25
- * - **A debug provider.** `registerGameDebug` publishes a JSON snapshot of the
26
- * run so external tooling (the editor's debug bridge, DevTools, an agent) can
27
- * read the phase instead of guessing it from the screen. Add your own fields
28
- * as you add state — keep everything JSON-serialisable.
29
- */
30
- import { Script, playable, registerGameDebug, type PropertySchema } from '@pix3/runtime';
31
-
32
- export class GameFlow extends Script {
33
- private phase: 'intro' | 'playing' | 'ended' = 'intro';
34
- private elapsed = 0;
35
- private disposeDebug: (() => void) | null = null;
36
- private disposeCommands: (() => void)[] = [];
37
-
38
- constructor(id: string, type: string) {
39
- super(id, type);
40
- this.config = {
41
- // Node id/name of the tap-to-start overlay.
42
- introNode: 'intro-overlay',
43
- // Node id/name of the end screen with the CTA button.
44
- endNode: 'end-screen',
45
- // Placeholder auto-win timer in seconds (0 = never; call finish() instead).
46
- autoWinAfterSec: 15,
47
- // Prototyping vent: start in `playing` with the gate already gone.
48
- skipIntro: false,
49
- };
50
- }
51
-
52
- static getPropertySchema(): PropertySchema {
53
- return {
54
- nodeType: 'GameFlow',
55
- properties: [
56
- {
57
- name: 'introNode',
58
- type: 'string',
59
- ui: { label: 'Intro Overlay', group: 'Flow' },
60
- getValue: s => (s as GameFlow).config.introNode,
61
- setValue: (s, v) => {
62
- (s as GameFlow).config.introNode = typeof v === 'string' ? v : '';
63
- },
64
- },
65
- {
66
- name: 'endNode',
67
- type: 'string',
68
- ui: { label: 'End Screen', group: 'Flow' },
69
- getValue: s => (s as GameFlow).config.endNode,
70
- setValue: (s, v) => {
71
- (s as GameFlow).config.endNode = typeof v === 'string' ? v : '';
72
- },
73
- },
74
- {
75
- name: 'autoWinAfterSec',
76
- type: 'number',
77
- ui: {
78
- label: 'Auto Win After (s)',
79
- description: 'Placeholder timer that reveals the end screen (0 = disabled)',
80
- group: 'Flow',
81
- min: 0,
82
- step: 1,
83
- },
84
- getValue: s => (s as GameFlow).config.autoWinAfterSec,
85
- setValue: (s, v) => {
86
- (s as GameFlow).config.autoWinAfterSec = Math.max(0, Number(v) || 0);
87
- },
88
- },
89
- {
90
- name: 'skipIntro',
91
- type: 'boolean',
92
- ui: {
93
- label: 'Skip Intro',
94
- description:
95
- 'Start already playing, with the tap gate hidden. For prototyping only — a shipped playable needs the tap to unlock audio.',
96
- group: 'Flow',
97
- },
98
- getValue: s => (s as GameFlow).config.skipIntro === true,
99
- setValue: (s, v) => {
100
- (s as GameFlow).config.skipIntro = v === true;
101
- },
102
- },
103
- ],
104
- groups: { Flow: { label: 'Game Flow', expanded: true } },
105
- };
106
- }
107
-
108
- onStart(): void {
109
- const commands = this.scene?.commands;
110
- this.disposeCommands = [
111
- commands?.register('start-game', () => this.start(), {
112
- description: 'Leave the intro overlay and begin play.',
113
- }),
114
- // `finish` is registered for the same reason `start-game` is: a test (and the
115
- // rest of the game) must be able to END the run by intent instead of waiting
116
- // out the placeholder timer — and a routine that reached in and called
117
- // `finish()` directly would prove the method works while skipping the wire
118
- // from the player to it. Replace the timer with your win/lose condition and
119
- // dispatch this from there.
120
- commands?.register('finish', () => this.finish(), {
121
- description: 'End the run and reveal the end screen.',
122
- }),
123
- commands?.register('restart', () => this.restart(), {
124
- description: 'Return to the tap-to-start gate and arm a fresh run.',
125
- }),
126
- ].filter((dispose): dispose is () => void => dispose !== undefined);
127
-
128
- this.disposeDebug = registerGameDebug({
129
- name: 'playable-2d',
130
- version: 1,
131
- // The registry IS the action list — never a second, hand-kept copy.
132
- actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
133
- // What "the start" means for this playable, so a test can put the game back
134
- // without restarting the scene (a restart keeps whatever a script parked in
135
- // module state; this does not). It is `restart()` plus the one thing that
136
- // lives OUTSIDE the scene and would otherwise leak into the next run: the
137
- // SDK's session `gameEnded` flag, which `playable.gameEnd()` latched.
138
- // `seed` is accepted and ignored — the placeholder flow has no randomness,
139
- // so this restores the same starting state, not the same sequence. Seed
140
- // your own RNG here once you add one.
141
- reset: () => {
142
- this.restart();
143
- playable.reset();
144
- },
145
- snapshot: () => ({
146
- phase: this.phase,
147
- elapsedSec: Math.round(this.elapsed * 100) / 100,
148
- autoWinAfterSec: Number(this.config.autoWinAfterSec) || 0,
149
- skipIntro: this.config.skipIntro === true,
150
- introVisible: this.isNodeVisible(String(this.config.introNode ?? '')),
151
- endVisible: this.isNodeVisible(String(this.config.endNode ?? '')),
152
- // Set by playable.gameEnd() — the CTA button reports the session as over.
153
- gameEnded: playable.hasGameEnded(),
154
- }),
155
- });
156
-
157
- if (this.config.skipIntro === true) {
158
- this.openTheGate();
159
- }
160
- }
161
-
162
- onDetach(): void {
163
- this.disposeDebug?.();
164
- this.disposeDebug = null;
165
- // The scene's registry is cleared on stop anyway; this covers the other
166
- // case — this script being detached while the scene keeps running.
167
- for (const dispose of this.disposeCommands) {
168
- dispose();
169
- }
170
- this.disposeCommands = [];
171
- super.onDetach();
172
- }
173
-
174
- onUpdate(dt: number): void {
175
- if (this.phase === 'intro') {
176
- const tapped = this.input?.pointerEvents.some(e => e.type === 'down') ?? false;
177
- if (tapped) {
178
- // Raised through the registry, so the tap-to-start gate shows up in the
179
- // command journal exactly like a dispatched `start-game` would.
180
- this.scene?.commands.dispatch('start-game');
181
- }
182
- return;
183
- }
184
-
185
- if (this.phase === 'playing') {
186
- const autoWin = Number(this.config.autoWinAfterSec) || 0;
187
- if (autoWin > 0) {
188
- this.elapsed += dt;
189
- if (this.elapsed >= autoWin) {
190
- this.finish();
191
- }
192
- }
193
- }
194
- }
195
-
196
- /** Intent: leave the intro overlay and begin play. Raised by the first tap. */
197
- start(): void {
198
- if (this.phase !== 'intro') {
199
- return;
200
- }
201
- this.openTheGate();
202
- }
203
-
204
- /**
205
- * Hide the gate and go to `playing`. Shared by the tap and by `skipIntro`, so the two
206
- * cannot drift into two slightly different notions of "the run has begun".
207
- */
208
- private openTheGate(): void {
209
- this.setNodeVisible(String(this.config.introNode ?? ''), false);
210
- this.phase = 'playing';
211
- }
212
-
213
- /** Intent: end the run and reveal the end screen. Call this from game code on win/lose. */
214
- finish(): void {
215
- if (this.phase === 'ended') {
216
- return;
217
- }
218
- this.phase = 'ended';
219
- this.setNodeVisible(String(this.config.endNode ?? ''), true);
220
- }
221
-
222
- /**
223
- * Intent: arm a fresh run — back at the tap-to-start gate, or straight into play when
224
- * `skipIntro` is on. A restart that re-raised a gate the project has turned off would
225
- * strand every routine that restarts between checks behind a tap nobody is going to make.
226
- */
227
- restart(): void {
228
- this.phase = 'intro';
229
- this.elapsed = 0;
230
- this.setNodeVisible(String(this.config.endNode ?? ''), false);
231
- this.setNodeVisible(String(this.config.introNode ?? ''), true);
232
- if (this.config.skipIntro === true) {
233
- this.openTheGate();
234
- }
235
- }
236
-
237
- private setNodeVisible(query: string, visible: boolean): void {
238
- if (!query) {
239
- return;
240
- }
241
- const node = this.findNode(query);
242
- if (node) {
243
- node.visible = visible;
244
- } else {
245
- console.warn(`[GameFlow] Node "${query}" not found.`);
246
- }
247
- }
248
-
249
- /** Own `visible` flag of a configured node, or null when it is not in the scene. */
250
- private isNodeVisible(query: string): boolean | null {
251
- if (!query) {
252
- return null;
253
- }
254
- return this.findNode(query)?.visible ?? null;
255
- }
256
- }
@@ -1,9 +0,0 @@
1
- id: playable-2d
2
- title: Playable 2D
3
- description: Portrait 2D playable-ad blank — tap-to-start intro (unlocks audio), gameplay placeholder, end screen with a store CTA button.
4
- # Flow recipe this template serves (see design/recipe.md).
5
- recipeId: recipe-playable-ad
6
- projectType: 2d
7
- targetPlatform: mobile
8
- viewport: { width: 1080, height: 1920 }
9
- order: 40
@@ -1,32 +0,0 @@
1
- # {{PROJECT_NAME}}
2
-
3
- A Pix3 project created from the **Playable 3D** 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 demo box and ground 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. `skipIntro: true` starts already playing while you iterate; turn it off before shipping, since that first tap is what unlocks audio.
11
- - **Bookkeeping, already built** (`scripts/GameRules.ts` + `scripts/ScoreHud.ts`) — score, lives, an optional clock, the win/lose decision and the HUD that shows them. Your mechanic calls `addScore(1)` / `loseLife()` / `finish(true)`, or emits `score-added` / `life-lost` on `hud-root`; don't write a second score counter or result screen.
12
- - **Readable state** — `GameFlow` publishes the one `registerGameDebug` snapshot (phase, timer, overlay visibility, session end, plus the rules' score/lives/time), so tooling reads the run's state instead of judging it from a screenshot. Add your own fields there — a second provider would silently replace this one.
13
- - **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.
14
- - **Canonical roots** — `game-root` holds the 3D scene, `hud-root` the 2D overlay layer (intro gate, HUD, end screen); routines and assertions address them by those ids.
15
-
16
- ## Project structure
17
-
18
- - `design/` — game design document and reference images
19
- - `design/tests/` — agent-testing material: `routines/*.json` (named step +
20
- assertion scripts, written on the highest channel the game offers) and
21
- `reachability.json` (controls proven reachable on a live run). The harness
22
- that runs them is still in development; format and location are fixed now.
23
- - `scenes/` — scene files; `main.pix3scene` is the startup scene
24
- - `scenes/ui/` — the full-screen overlays (`intro.pix3scene`, `end-screen.pix3scene`),
25
- instanced into `main.pix3scene` with `visible: false` so the editor opens on the
26
- game. That flag is editor-only; `initiallyVisible` on each overlay's root is what
27
- play mode reads. Edit an overlay in its own tab, or tick the eye on its instance
28
- node to preview it composited — just don't save that tick.
29
- - `sprites/` — images and textures
30
- - `scripts/` — game scripts (`export class X extends Script`, used as `user:X`)
31
- - `audio/` — music and sound effects
32
- - `AGENTS.md` — rules and pointers for AI agents working on this project
@@ -1,127 +0,0 @@
1
- # Recipe: recipe-scene-3d
2
-
3
- ## What this is
4
-
5
- The only **3D** recipe: a portrait scene with a real 3D stage — perspective
6
- `Camera3D`, a key light and an ambient fill, a ground slab and one hero solid —
7
- under a 2D UI layer with a tap-to-start gate and an end screen with a CTA.
8
-
9
- Reach for it whenever the idea is genuinely three-dimensional — a cube you carve,
10
- a stack you topple, a solid you inspect from all sides. Every other recipe is 2D,
11
- and a 3D idea faked with isometric sprites is not the same game.
12
-
13
- The gameplay is a placeholder — `hero-box` spinning on a `core:Rotate`, plus an
14
- auto-win timer. Replace both; keep the camera, the lights and the gate.
15
-
16
- **The bookkeeping is already built.** `GameRules` keeps score, lives and the clock and
17
- decides win/lose; `ScoreHud` displays them. Your first increment is the *mechanic*: call
18
- `addScore(1)` / `loseLife()` / `finish(true)`, or emit `score-added` / `life-lost` on
19
- `hud-root`. Never write a second score counter, HUD label or result screen.
20
-
21
- **Lighting is load-bearing.** Lit materials with no light render pure black, and
22
- the editor viewport hides that by adding fallback lights the running game has not.
23
- `key-light` and `ambient-light` are why this scene is visible — never delete them.
24
- A mesh that comes out black is reported by the scene lint (`sceneIssues` in
25
- `scene_tree` / `game_observe`).
26
-
27
- **Mobile is the default target.** Meshes use `material.type: lambert`
28
- (diffuse-only), not PBR: the shapes read the same at a fraction of the per-pixel
29
- cost, and new geometry inherits it. `standard` (PBR) is for a `desktop`
30
- `targetPlatform` — switch one mesh with `set_property <node> materialType
31
- standard` when the user asks for a high-end look. `basic` (unlit) is cheaper
32
- still and the one family that cannot render black for want of a light.
33
-
34
- ## Node map
35
-
36
- | id | what |
37
- | --- | --- |
38
- | `game-root` | 3D scene root (`Node3D`) |
39
- | `main-camera` | `Camera3D`, perspective, looking down at the stage from `[0, 3, 8]` |
40
- | `key-light` | `DirectionalLightNode` — the shading light; angle it, don't remove it |
41
- | `ambient-light` | `AmbientLightNode` — the fill that keeps unlit faces readable |
42
- | `ground` | `GeometryMesh` box, 12×0.2×12, the floor slab |
43
- | `hero-box` | `GeometryMesh` box placeholder, spinning on `core:Rotate` |
44
- | `hud-root` | 2D UI layer (`Group2D`, stretched); hosts `GameFlow` (intro → playing → ended), `GameRules` (score/lives/win-lose) and `ScoreHud` (display only) |
45
- | `hud-label` | in-game text line |
46
- | `score-label` | `SCORE n`, driven by the `score-changed` signal |
47
- | `time-label` | seconds — counts up with no time limit, down with one |
48
- | `lives-bar` | `Bar2D` fed by `lives-changed` |
49
- | `intro-overlay` → `intro-dim`, `intro-label` | the tap gate (`scenes/ui/intro.pix3scene`, instanced into `hud-root`); the first tap hides it, starts the game, unlocks browser audio |
50
- | `end-screen` → `end-dim`, `end-label`, `cta-button` | end screen (`scenes/ui/end-screen.pix3scene`, instanced into `hud-root`), `initiallyVisible: false` |
51
- | `cta-button` | hosts `CtaButton` — `playable.gameEnd()` + a `[CtaButton] CTA clicked` log; the store call belongs to the ad network SDK, not to the template |
52
-
53
- End a run with `GameRules.finish(won)` — it writes the outcome text and dispatches
54
- `GameFlow`'s `finish`. One end screen, one phase machine, one debug snapshot (the
55
- rules' numbers ride inside GameFlow's).
56
-
57
- ## Placeholders
58
-
59
- | role | file | node |
60
- | --- | --- | --- |
61
- | hero | (none — `hero-box` is untextured geometry) | `hero-box` |
62
-
63
- ## Tunables
64
-
65
- ```yaml
66
- tunables:
67
- autoWinAfterSec: { node: hud-root, component: "user:GameFlow", property: autoWinAfterSec, min: 0, max: 120, default: 15 }
68
- introNode: { node: hud-root, component: "user:GameFlow", property: introNode, default: intro-overlay }
69
- endNode: { node: hud-root, component: "user:GameFlow", property: endNode, default: end-screen }
70
- skipIntro: { node: hud-root, component: "user:GameFlow", property: skipIntro, default: false }
71
- winMode: { node: hud-root, component: "user:GameRules", property: winMode, default: score }
72
- targetScore: { node: hud-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
73
- timeLimitSec: { node: hud-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
74
- startingLives: { node: hud-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
75
- keyLightIntensity: { node: key-light, property: intensity, min: 0, max: 4, default: 1.2 }
76
- ambientIntensity: { node: ambient-light, property: intensity, min: 0, max: 2, default: 0.5 }
77
- ```
78
-
79
- `component` present → `set_component_property`; absent → `set_property`.
80
-
81
- `skipIntro: true` starts already playing, gate hidden — a prototyping vent, off before
82
- shipping: that first tap is what unlocks browser audio. Mesh colour is not a declared
83
- tunable — it lives under `material.color` and is edited with
84
- `set_property <node> color "#rrggbb"`.
85
-
86
- ## Extension points
87
-
88
- - **Real geometry.** `create_node` more `GeometryMesh` nodes (`box`, `sphere`,
89
- `cylinder`, …) with `position3`, or point `hero-box` at a `.glb` as a
90
- `MeshInstance3D`. A grid or stack of solids is a loop over `create_node`.
91
- - **Picking.** 3D taps come from the raycast API, not 2D hit-testing: read the
92
- pointer in a script and ray-test the meshes. That is the backbone of every
93
- "tap the block" mechanic.
94
- - **Camera moves.** Orbit or pull back by animating `main-camera`'s transform, or
95
- add a `VirtualCamera3D` rig if the game needs blends between framings.
96
- - **Real gameplay.** Set `autoWinAfterSec: 0` and let `GameRules` end the run:
97
- `addScore`/`score-added`, `loseLife`/`life-lost`, and a win condition picked with
98
- `winMode` + `targetScore` + `timeLimitSec` instead of one you write.
99
-
100
- ## Do not touch
101
-
102
- - **`key-light` / `ambient-light`.** Deleting or disabling them makes every lit
103
- mesh black. Re-colour and re-angle freely; keep at least one enabled light.
104
- - **`main-camera`.** A 3D scene with no enabled `Camera3D` draws nothing at all.
105
- - **The `hud-root` layout.** Stretched to the safe area on purpose; nest new UI
106
- inside it, never add a second full-screen root.
107
- - **`GameFlow`'s `introNode` / `endNode` ids.** They address nodes by id: renaming a
108
- node without updating them breaks the phase machine silently. Same for `ScoreHud`'s
109
- `sourceNode` / label ids.
110
- - **The overlay instances.** `intro-overlay` / `end-screen` in `main.pix3scene` are
111
- `instance:` nodes marked `visible: false` — an editor-only hide so the scene opens
112
- on the stage. Keep it; `initiallyVisible` lives in the overlay file (`true` for
113
- the gate, `false` for the end screen), never as an instance override.
114
- - **A second `registerGameDebug`.** `GameFlow` publishes the only one (rules' numbers
115
- merged in); a second silently replaces it. Add fields to that snapshot.
116
-
117
- ## Verify
118
-
119
- - `play_start`, then `game_observe` — check `sceneIssues` is **absent**. A
120
- `lit-material-no-light` or `no-camera-3d` entry means the scene cannot draw.
121
- - Tap the gate: `game_input {type:'tap', target:'intro-overlay'}` and confirm
122
- `intro-overlay` goes invisible and the game phase advances.
123
- - Read scoring back as data: the snapshot carries `score`, `lives`, `timeLeftSec`
124
- and `outcome` next to `phase` — never judge a counter from a picture.
125
- - Judge the win with `game_run` and an `until` that names what success is (e.g.
126
- `end-screen` visible), not with a screenshot — the editor viewport adds
127
- fallback lights, so a picture cannot prove the running game is lit.
@@ -1,4 +0,0 @@
1
- {
2
- "version": 1,
3
- "proven": []
4
- }
@@ -1,32 +0,0 @@
1
- {
2
- "name": "intro-to-cta",
3
- "description": "Tap through the intro gate, end the run by intent, and press the CTA on the end screen.",
4
- "note": "Run it with game_run {routine:'intro-to-cta'}. Steps are the game_input vocabulary (tap/key/keys/drag/hover/wait/invoke) plus {type:'command', name} for an intent the scene registered; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step. A step's `why` and this `note` are documentation — the runner ignores both — but they are why the routine also reads as a checklist. Note what is NOT here: no step calls a component method directly. Ending the run goes through the `finish` command, because a routine that called the handler would prove the handler works while skipping every wire between the player and it. GameFlow rides the 2D overlay layer (hud-root), above the 3D game-root.",
5
- "scope": "scenes/main.pix3scene",
6
- "params": {},
7
- "uses": ["game-root", "hud-root", "intro-overlay", "end-screen", "cta-button", "hero-box"],
8
- "steps": [
9
- {
10
- "type": "tap",
11
- "target": "intro-overlay",
12
- "why": "The tap gate is the gesture under test: it hides the overlay and unlocks browser audio, so it is driven as a real tap rather than by dispatching start-game."
13
- },
14
- {
15
- "type": "command",
16
- "name": "finish",
17
- "why": "Reveal the end screen by intent instead of waiting out autoWinAfterSec — the timer is a placeholder every real playable replaces, and 15 s of wall clock in every run is a budget nobody gets back."
18
- },
19
- {
20
- "type": "tap",
21
- "target": "cta-button",
22
- "holdMs": 700,
23
- "why": "Button2D needs a real press; this proves the button is on screen and hittable, not just present in the tree."
24
- }
25
- ],
26
- "expect": [
27
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "ended" },
28
- { "kind": "gameState", "path": "introVisible", "op": "eq", "value": false },
29
- { "kind": "gameState", "path": "endVisible", "op": "eq", "value": true },
30
- { "kind": "gameState", "path": "gameEnded", "op": "eq", "value": true }
31
- ]
32
- }