@pix3/cli 1.6.4 → 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,94 +0,0 @@
1
- /**
2
- * ScoreHud — signal-driven HUD. Display only, no game logic.
3
- *
4
- * Listens on `sourceNode` (the node carrying `GameRules`) for:
5
- * `score-changed` (score)
6
- * `lives-changed` (lives, maxLives)
7
- * `time-changed` (seconds)
8
- *
9
- * If a HUD widget is missing from the scene it is quietly skipped, so you can
10
- * delete the timer or the lives bar without touching any script.
11
- */
12
- import { Bar2D, Label2D, Script, type PropertySchema } from '@pix3/runtime';
13
-
14
- export class ScoreHud extends Script {
15
- constructor(id: string, type: string) {
16
- super(id, type);
17
- this.config = {
18
- // Node that emits the HUD signals (the GameRules host).
19
- sourceNode: 'game-root',
20
- scoreLabel: 'score-label',
21
- scoreFormat: 'SCORE {value}',
22
- timeLabel: 'time-label',
23
- timeFormat: '{value}',
24
- livesBar: 'lives-bar',
25
- };
26
- }
27
-
28
- static getPropertySchema(): PropertySchema {
29
- const str = (name: string, label: string, description?: string) => ({
30
- name,
31
- type: 'string' as const,
32
- ui: { label, group: 'HUD', description },
33
- getValue: (s: unknown) => (s as ScoreHud).config[name],
34
- setValue: (s: unknown, v: unknown) => {
35
- (s as ScoreHud).config[name] = typeof v === 'string' ? v : '';
36
- },
37
- });
38
-
39
- return {
40
- nodeType: 'ScoreHud',
41
- properties: [
42
- str('sourceNode', 'Source Node', 'Node that emits score-changed / lives-changed / time-changed'),
43
- str('scoreLabel', 'Score Label'),
44
- str('scoreFormat', 'Score Format', '{value} is replaced by the score'),
45
- str('timeLabel', 'Time Label'),
46
- str('timeFormat', 'Time Format', '{value} is replaced by whole seconds'),
47
- str('livesBar', 'Lives Bar'),
48
- ],
49
- groups: { HUD: { label: 'HUD', expanded: true } },
50
- };
51
- }
52
-
53
- onStart(): void {
54
- const source = this.findNode(String(this.config.sourceNode ?? ''));
55
- if (!source) {
56
- console.warn(`[ScoreHud] Source node "${this.config.sourceNode}" not found.`);
57
- return;
58
- }
59
- source.connect('score-changed', this, (...args: unknown[]) => this.setScore(Number(args[0]) || 0));
60
- source.connect('time-changed', this, (...args: unknown[]) => this.setTime(Number(args[0]) || 0));
61
- source.connect('lives-changed', this, (...args: unknown[]) =>
62
- this.setLives(Number(args[0]) || 0, Number(args[1]) || 0)
63
- );
64
- // Ask for the opening values now that the listeners are live: GameRules
65
- // starts before this HUD (parent components tick before children).
66
- source.emit('hud-ready');
67
- }
68
-
69
- private setScore(score: number): void {
70
- const label = this.findNode(String(this.config.scoreLabel ?? ''));
71
- if (label instanceof Label2D) {
72
- label.setText(String(this.config.scoreFormat ?? '{value}').replace('{value}', String(Math.round(score))));
73
- }
74
- }
75
-
76
- private setTime(seconds: number): void {
77
- const label = this.findNode(String(this.config.timeLabel ?? ''));
78
- if (label instanceof Label2D) {
79
- label.setText(
80
- String(this.config.timeFormat ?? '{value}').replace('{value}', String(Math.max(0, Math.ceil(seconds))))
81
- );
82
- }
83
- }
84
-
85
- private setLives(lives: number, maxLives: number): void {
86
- const bar = this.findNode(String(this.config.livesBar ?? ''));
87
- if (bar instanceof Bar2D) {
88
- if (maxLives > 0) {
89
- bar.maxValue = maxLives;
90
- }
91
- bar.setValue(lives);
92
- }
93
- }
94
- }
@@ -1,10 +0,0 @@
1
- id: recipe-grid-3d
2
- title: Grid 3D
3
- description: A solid block of cubes you carve by tapping — clear every cube except the hidden core ones. Voxel carving, 3D minesweeper, layer puzzles, tap-to-mine.
4
- projectType: 3d
5
- targetPlatform: mobile
6
- viewport: { width: 1080, height: 1920 }
7
- # The build/full-run entry scene (Project Settings → Default Export Scene Path).
8
- # The menu boots first; PLAY transitions to main.pix3scene (the board).
9
- entryScene: scenes/menu.pix3scene
10
- order: 63
@@ -1,39 +0,0 @@
1
- # Tapper 2D
2
-
3
- A playable skeleton: objects fall into the field, tapping them is the whole
4
- game, one bad type punishes, and a countdown ends the run. It plays as
5
- shipped — press play in `scenes/main.pix3scene` before changing anything.
6
-
7
- **`design/recipe.md` is the contract.** Stable node ids, tunables with ranges,
8
- extension points and what must not be renamed. Read it first.
9
-
10
- ## Layout
11
-
12
- ```
13
- scenes/menu.pix3scene entry / export scene — PLAY transitions to the game
14
- scenes/main.pix3scene the GAME (editor startup scene — iterate here)
15
- scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
16
- scenes/prefabs/*.pix3scene what the spawners instantiate
17
- scripts/Spawner.ts timed instantiate + drift + despawn
18
- scripts/TouchRules.ts a tap → semantic signals
19
- scripts/ScoreHud.ts signals → HUD widgets
20
- scripts/GameRules.ts score / lives / timer / win / lose / end flow
21
- scripts/MenuFlow.ts menu PLAY button
22
- sprites/ph-*.png near-white placeholders (tinted via core:tint)
23
- ```
24
-
25
- Same script names and roles as the other recipes — `Spawner`, `TouchRules`,
26
- `ScoreHud`, `GameRules` mean the same thing everywhere, only their contact
27
- source differs (here: a pointer-down hit-test instead of a proximity test).
28
-
29
- ## The rule that keeps it workable
30
-
31
- One file, one responsibility, 70–140 lines. Small files can be edited
32
- surgically; monoliths get rewritten wholesale and lose the changes that were
33
- already working. When you add a mechanic, add a script.
34
-
35
- ```
36
- TouchRules --touch-scored/touch-damaged--> GameRules --score-changed--> ScoreHud
37
- --lives-changed-->
38
- --time-changed-->
39
- ```
@@ -1,132 +0,0 @@
1
- # Recipe: recipe-tapper-2d
2
-
3
- ## What this is
4
-
5
- Objects fall into a field; tapping them is the whole game. One "bad" type
6
- punishes instead of scoring. A countdown ends the run and a target score decides
7
- win or lose. Playable before any edit.
8
-
9
- Same script names and roles as the other recipes — `Spawner`, `TouchRules`,
10
- `ScoreHud`, `GameRules` — so what you learn here transfers.
11
-
12
- ## Node map
13
-
14
- | id | what |
15
- | --- | --- |
16
- | `game-root` | root; hosts `GameRules` + `TouchRules`; all HUD signals emitted here |
17
- | `game-background` | full-screen `ColorRect2D` (palette background) |
18
- | `board` | the field (`Group2D`) |
19
- | `board-floor` | the field's visible plate |
20
- | `spawner-targets` | spawn band for tappable things; hosts `Spawner` |
21
- | `spawner-hazards` | spawn band for the bad type; hosts `Spawner` |
22
- | `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
23
- | `score-label` / `time-label` / `lives-bar` / `menu-button` | HUD |
24
- | `result-overlay` / `result-label` / `retry-button` | end screen in `scenes/ui/result.pix3scene`, instanced into main hidden (`visible: false` is editor-only) |
25
-
26
- Signals: `TouchRules` emits `touch-scored` / `touch-damaged` on `game-root`;
27
- `GameRules` answers `score-changed`, `lives-changed`, `time-changed`,
28
- `game-won` / `game-lost`.
29
-
30
- ## Placeholders
31
-
32
- Near-white PNGs carrying a `core:tint` effect: set `effects[0].params.color` and
33
- the multiply gives you that palette colour.
34
-
35
- | role | file | node/prefab |
36
- | --- | --- | --- |
37
- | collectible | `sprites/ph-target.png` | `prefabs/target.pix3scene` |
38
- | threat | `sprites/ph-hazard.png` | `prefabs/hazard.pix3scene` |
39
-
40
- ## Tunables
41
-
42
- ```yaml
43
- tunables:
44
- targetRate: { node: spawner-targets, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 0.85 }
45
- targetMaxAlive: { node: spawner-targets, component: "user:Spawner", property: maxAlive, min: 1, max: 200, default: 8 }
46
- targetFallSpeed: { node: spawner-targets, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -230 }
47
- targetLifetime: { node: spawner-targets, component: "user:Spawner", property: lifetimeSec, min: 0, max: 120, default: 8 }
48
- hazardRate: { node: spawner-hazards, component: "user:Spawner", property: intervalSec, min: 0.05, max: 20, default: 2.2 }
49
- hazardFallSpeed: { node: spawner-hazards, component: "user:Spawner", property: driftY, min: -3000, max: 3000, default: -260 }
50
- tapRadius: { node: game-root, component: "user:TouchRules", property: tapRadius, min: 0, max: 400, default: 40 }
51
- missPenalty: { node: game-root, component: "user:TouchRules", property: missPenalty, min: 0, max: 10, default: 0 }
52
- touchRules: { node: game-root, component: "user:TouchRules", property: rules, default: "target:score:1;hazard:damage:1" }
53
- winMode: { node: game-root, component: "user:GameRules", property: winMode, default: time }
54
- targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 18 }
55
- timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 30 }
56
- startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
57
- bgColor: { node: game-background, property: color, default: "#141a2e" }
58
- boardColor: { node: board-floor, property: color, default: "#1d2743" }
59
- ```
60
-
61
- `component` present → `set_component_property`; absent → `set_property`. The
62
- schema clamps out-of-range values rather than rejecting them.
63
-
64
- ## Extension points
65
-
66
- - **Popping instead of falling (whack-a-mole).** Set `targetFallSpeed: 0` and
67
- `targetLifetime` to ~1.2 s: things appear in place and vanish if not tapped.
68
- Set `gridSnap` on the spawner (e.g. 220) and raise `spawnHeight` to get a mole
69
- grid instead of a band.
70
- - **Combos / multipliers.** A new script on `hud` that listens for
71
- `touch-scored` on `game-root`, counts hits inside a time window, and adds
72
- only the **extra** points: `game-root.emit('touch-scored', amount * (m - 1), x, y)`
73
- under a re-entrancy guard (`if (this.emitting) return; this.emitting = true;
74
- try { emit(...) } finally { this.emitting = false; }`). `GameRules` already
75
- counted the base `amount` and needs no change. Show the multiplier through a
76
- signal of its own (`combo-changed`), never through `touch-scored`.
77
- **The trap:** a listener on `touch-scored` that emits `touch-scored` on the
78
- same node is called again by its own emit — without the guard, the first tap
79
- recurses until the stack overflows (and re-emitting the *full* amount would
80
- double-count the base). No offline check sees it: `pix3 check` and
81
- `pix3 smoke` are green, because smoke taps nothing. Say so, and have the human
82
- tap three targets in a row.
83
- - **A third type.** Copy a prefab, give its `core:Hitbox2D` a new group, add a
84
- node with a third `Spawner` (`create_node` + `add_component`), and append
85
- `<group>:score:5` to `touchRules`.
86
- - **Punish misses.** Raise `missPenalty` — a tap that hits nothing then costs a
87
- life, which turns a relaxed collector into a precision game.
88
- - **Sudden death.** `startingLives: 1`, or `winMode: survive` with a longer
89
- `timeLimitSec` so surviving the clock is the win.
90
-
91
- Replacing a whole script the list above names is expected: use `fs_write` with
92
- `overwrite: true` and a short `reason` (the size guard only blocks *silent*
93
- wholesale rewrites).
94
-
95
- ## Ending a run belongs to GameRules
96
-
97
- The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
98
- start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
99
- writes the result text, enables RETRY and freezes the `freezeNodes` subtrees
100
- (every component on them and on what they spawned). For a custom
101
- win/lose condition call `finish(true|false)` on that component — do NOT show the
102
- overlay yourself. A hand-rolled ending leaves RETRY on screen with its handler bound
103
- and `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
104
- `control: { enabled: false }`; that is the tell).
105
-
106
- ## Do not touch
107
-
108
- - The node ids above (rename `name`, never `id`), and the signal names.
109
- - Component config on a **prefab instance** — the editor locks it. Prefabs carry
110
- fixed defaults only (their hitbox group); tunables live on plain nodes in
111
- `main.pix3scene`.
112
- - The prefab hitbox groups must stay in sync with `touchRules`; the group is the
113
- only thing that separates "good" from "bad".
114
-
115
- ## Playtest contract
116
-
117
- terminalVisual: static
118
- terminalRestartRoutine: terminal-retry
119
-
120
- After the result overlay appears, the screen should remain still until RETRY. The
121
- playtest samples five rendered frames at 15-frame intervals after the terminal
122
- phase has held for 120 frames. The `terminal-retry` routine physically taps RETRY
123
- and checks that the game returns to its playing phase.
124
-
125
- ## Verify
126
-
127
- 1. `play_start` on `scenes/main.pix3scene`.
128
- 2. After ~2 s, children exist under `spawner-targets` and are falling.
129
- 3. `game_input` a tap on one of them → it disappears, `score-label` climbs, and
130
- the punch-scale fires.
131
- 4. Tap three hazards → `lives-bar` empties and `result-overlay` shows
132
- `GAME OVER`; `retry-button` restarts the scene.
@@ -1,4 +0,0 @@
1
- {
2
- "version": 1,
3
- "proven": []
4
- }
@@ -1,23 +0,0 @@
1
- {
2
- "name": "terminal-retry",
3
- "description": "From the result screen, physically tap RETRY and check that play resumes.",
4
- "scope": "scenes/main.pix3scene",
5
- "params": {},
6
- "uses": ["result-overlay", "retry-button"],
7
- "steps": [
8
- {
9
- "type": "tap",
10
- "target": "retry-button",
11
- "holdMs": 200,
12
- "why": "A real pointer press proves the button is visible, enabled and reachable."
13
- },
14
- {
15
- "type": "wait",
16
- "frames": 30,
17
- "why": "Let the scene transition finish before reading the new game's phase."
18
- }
19
- ],
20
- "expect": [
21
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" }
22
- ]
23
- }
@@ -1,269 +0,0 @@
1
- # Pix3 Scene File (YAML)
2
- # Tapper 2D — the GAME scene, and the editor's startup scene: iterate here.
3
- # A full build boots the MENU (menu.pix3scene), whose PLAY transitions here.
4
- #
5
- # Objects fall into the board; tapping them is the whole game. A tap is
6
- # hit-tested (scripts/TouchRules.ts) against the `core:Hitbox2D` group each
7
- # prefab carries: `target` scores, `hazard` costs a life.
8
- #
9
- # Stable node ids (design/recipe.md is the contract — do not rename these):
10
- # game-root, game-background, board, spawner-targets, spawner-hazards,
11
- # hud, score-label, time-label, lives-bar, menu-button,
12
- # result-overlay, result-label, retry-button (scenes/ui/result.pix3scene)
13
- #
14
- # Full-screen UI lives in its own file under scenes/ui/ and is instanced here
15
- # hidden (`visible: false` = editor only), so opening this scene shows the game
16
- # rather than a GAME OVER card.
17
- #
18
- # 2D coordinates: origin at the canvas centre, X right, Y UP. Node order in the
19
- # tree is paint order (later/deeper draws on top).
20
- version: 1.0.0
21
- metadata:
22
- author: Pix3 Recipe
23
- description: Tapper gameplay — falling objects, tap the good ones, avoid the bad
24
- root:
25
- - id: game-root
26
- type: Group2D
27
- name: Game Root
28
- properties:
29
- width: 1080
30
- height: 1920
31
- layout:
32
- enabled: true
33
- horizontalAlign: stretch
34
- verticalAlign: stretch
35
- components:
36
- - id: game-rules
37
- type: user:GameRules
38
- enabled: true
39
- config:
40
- winMode: time
41
- targetScore: 18
42
- timeLimitSec: 30
43
- startingLives: 3
44
- resultNode: result-overlay
45
- resultLabel: result-label
46
- winText: "YOU WIN!"
47
- loseText: "GAME OVER"
48
- retryButton: retry-button
49
- menuButton: menu-button
50
- menuScene: res://scenes/menu.pix3scene
51
- gameScene: res://scenes/main.pix3scene
52
- freezeNodes: spawner-targets,spawner-hazards
53
- - id: touch-rules
54
- type: user:TouchRules
55
- enabled: true
56
- config:
57
- rules: "target:score:1;hazard:damage:1"
58
- tapRadius: 40
59
- missPenalty: 0
60
- consume: true
61
- scoreSound: ""
62
- damageSound: ""
63
- children:
64
- - id: game-background
65
- type: ColorRect2D
66
- name: Game Background
67
- properties:
68
- width: 1080
69
- height: 1920
70
- color: "#141a2e"
71
- transform:
72
- position: [0, 0]
73
- scale: [1, 1]
74
- rotation: 0
75
- layout:
76
- enabled: true
77
- horizontalAlign: stretch
78
- verticalAlign: stretch
79
- children: []
80
- - id: board
81
- type: Group2D
82
- name: Board
83
- properties:
84
- width: 980
85
- height: 1480
86
- transform:
87
- position: [0, -80]
88
- scale: [1, 1]
89
- rotation: 0
90
- children:
91
- - id: board-floor
92
- type: ColorRect2D
93
- name: Board Floor
94
- properties:
95
- width: 980
96
- height: 1480
97
- color: "#1d2743"
98
- transform:
99
- position: [0, 0]
100
- scale: [1, 1]
101
- rotation: 0
102
- children: []
103
- - id: spawner-targets
104
- type: Group2D
105
- name: Spawner Targets
106
- properties:
107
- width: 900
108
- height: 60
109
- transform:
110
- position: [0, 690]
111
- scale: [1, 1]
112
- rotation: 0
113
- components:
114
- - id: target-spawner
115
- type: user:Spawner
116
- enabled: true
117
- config:
118
- prefab: res://scenes/prefabs/target.pix3scene
119
- parentNode: ""
120
- intervalSec: 0.85
121
- intervalJitter: 0.3
122
- maxAlive: 8
123
- spawnWidth: 820
124
- spawnHeight: 60
125
- gridSnap: 0
126
- driftX: 0
127
- driftY: -230
128
- lifetimeSec: 8
129
- despawnRadius: 1450
130
- startDelaySec: 0.3
131
- children: []
132
- - id: spawner-hazards
133
- type: Group2D
134
- name: Spawner Hazards
135
- properties:
136
- width: 900
137
- height: 60
138
- transform:
139
- position: [0, 690]
140
- scale: [1, 1]
141
- rotation: 0
142
- components:
143
- - id: hazard-spawner
144
- type: user:Spawner
145
- enabled: true
146
- config:
147
- prefab: res://scenes/prefabs/hazard.pix3scene
148
- parentNode: ""
149
- intervalSec: 2.2
150
- intervalJitter: 0.8
151
- maxAlive: 5
152
- spawnWidth: 820
153
- spawnHeight: 60
154
- gridSnap: 0
155
- driftX: 0
156
- driftY: -260
157
- lifetimeSec: 8
158
- despawnRadius: 1450
159
- startDelaySec: 1.5
160
- children: []
161
- - id: hud
162
- type: CanvasLayer2D
163
- name: HUD
164
- properties:
165
- width: 1080
166
- height: 1920
167
- layout:
168
- enabled: true
169
- horizontalAlign: stretch
170
- verticalAlign: stretch
171
- components:
172
- - id: score-hud
173
- type: user:ScoreHud
174
- enabled: true
175
- config:
176
- sourceNode: game-root
177
- scoreLabel: score-label
178
- scoreFormat: "SCORE {value}"
179
- timeLabel: time-label
180
- timeFormat: "{value}"
181
- livesBar: lives-bar
182
- children:
183
- - id: score-label
184
- type: Label2D
185
- name: Score Label
186
- properties:
187
- label: "SCORE 0"
188
- labelFontSize: 44
189
- labelColor: "#ffffff"
190
- labelAlign: left
191
- transform:
192
- position: [-340, 850]
193
- scale: [1, 1]
194
- rotation: 0
195
- layout:
196
- enabled: true
197
- horizontalAlign: left
198
- verticalAlign: top
199
- children: []
200
- - id: time-label
201
- type: Label2D
202
- name: Time Label
203
- properties:
204
- label: "30"
205
- labelFontSize: 44
206
- labelColor: "#f5ae39"
207
- labelAlign: center
208
- transform:
209
- position: [0, 850]
210
- scale: [1, 1]
211
- rotation: 0
212
- layout:
213
- enabled: true
214
- horizontalAlign: center
215
- verticalAlign: top
216
- children: []
217
- - id: lives-bar
218
- type: Bar2D
219
- name: Lives Bar
220
- properties:
221
- width: 240
222
- height: 28
223
- minValue: 0
224
- maxValue: 3
225
- value: 3
226
- barColor: "#ff4d6d"
227
- backBackgroundColor: "#2a3352"
228
- showBorder: false
229
- transform:
230
- position: [340, 855]
231
- scale: [1, 1]
232
- rotation: 0
233
- layout:
234
- enabled: true
235
- horizontalAlign: right
236
- verticalAlign: top
237
- children: []
238
- - id: menu-button
239
- type: Button2D
240
- name: Menu Button
241
- properties:
242
- width: 200
243
- height: 72
244
- label: "MENU"
245
- labelFontSize: 30
246
- labelColor: "#ffffff"
247
- backgroundColor: "#2a3352"
248
- hoverColor: "#3a446b"
249
- pressedColor: "#1d2743"
250
- buttonAction: back-to-menu
251
- transform:
252
- position: [-400, -880]
253
- scale: [1, 1]
254
- rotation: 0
255
- layout:
256
- enabled: true
257
- horizontalAlign: left
258
- verticalAlign: bottom
259
- children: []
260
- # Win/lose screen — authored in scenes/ui/result.pix3scene so this
261
- # scene opens on the GAME, not on a GAME OVER card. `visible: false`
262
- # hides it in the EDITOR only; `initiallyVisible` (in the overlay
263
- # file) is what play mode reads, and GameRules reveals it on the
264
- # win/lose transition. Tick the eye to preview it over the field.
265
- - id: result-overlay
266
- name: Result Overlay
267
- instance: res://scenes/ui/result.pix3scene
268
- properties:
269
- visible: false
@@ -1,100 +0,0 @@
1
- # Pix3 Scene File (YAML)
2
- # Tapper 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. PLAY transitions to the game scene (main.pix3scene) via a fade.
5
- # Wired by scripts/MenuFlow.ts. Iterate on the GAME in main.pix3scene.
6
- version: 1.0.0
7
- metadata:
8
- author: Pix3 Recipe
9
- description: Menu scene — entry point; PLAY transitions to the game
10
- root:
11
- - id: menu-root
12
- type: Group2D
13
- name: Menu Root
14
- properties:
15
- width: 1080
16
- height: 1920
17
- layout:
18
- enabled: true
19
- horizontalAlign: stretch
20
- verticalAlign: stretch
21
- components:
22
- - id: menu-flow
23
- type: user:MenuFlow
24
- enabled: true
25
- config:
26
- gameScene: res://scenes/main.pix3scene
27
- playButton: play-button
28
- children:
29
- - id: menu-background
30
- type: ColorRect2D
31
- name: Menu Background
32
- properties:
33
- width: 1080
34
- height: 1920
35
- color: "#141a2e"
36
- transform:
37
- position: [0, 0]
38
- scale: [1, 1]
39
- rotation: 0
40
- layout:
41
- enabled: true
42
- horizontalAlign: stretch
43
- verticalAlign: stretch
44
- children: []
45
- - id: title-label
46
- type: Label2D
47
- name: Title Label
48
- properties:
49
- label: "{{PROJECT_NAME}}"
50
- labelFontSize: 96
51
- labelColor: "#ffffff"
52
- labelAlign: center
53
- transform:
54
- position: [0, 420]
55
- scale: [1, 1]
56
- rotation: 0
57
- layout:
58
- enabled: true
59
- horizontalAlign: center
60
- verticalAlign: center
61
- children: []
62
- - id: subtitle-label
63
- type: Label2D
64
- name: Subtitle Label
65
- properties:
66
- label: "TAP FAST. MISS NOTHING."
67
- labelFontSize: 40
68
- labelColor: "#93a3c9"
69
- labelAlign: center
70
- transform:
71
- position: [0, 300]
72
- scale: [1, 1]
73
- rotation: 0
74
- layout:
75
- enabled: true
76
- horizontalAlign: center
77
- verticalAlign: center
78
- children: []
79
- - id: play-button
80
- type: Button2D
81
- name: Play Button
82
- properties:
83
- width: 480
84
- height: 140
85
- label: "PLAY"
86
- labelFontSize: 56
87
- labelColor: "#141a2e"
88
- backgroundColor: "#f5ae39"
89
- hoverColor: "#ffc862"
90
- pressedColor: "#d4922a"
91
- buttonAction: play
92
- transform:
93
- position: [0, -60]
94
- scale: [1, 1]
95
- rotation: 0
96
- layout:
97
- enabled: true
98
- horizontalAlign: center
99
- verticalAlign: center
100
- children: []