@pix3/cli 1.6.0 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-CBFYKDS7.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-CFQFEI52.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +39 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +43 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
@@ -1,106 +1,106 @@
1
- /**
2
- * MenuFlow — drives the menu scene (the project's entry/export scene).
3
- *
4
- * PLAY transitions to the game scene with a fade. That is all it does: keep the
5
- * menu dumb so the game scene stays directly playable on its own (agents and
6
- * the editor open `main.pix3scene` and press play without going through here).
7
- *
8
- * PLAY does not call `startGame()` directly: it dispatches the command
9
- * `start-game` (`scene.commands`). A test can then leave the menu without a tap,
10
- * every raised intent lands in `scene.commands.log` with its frame, and one real
11
- * tap is enough to prove the button is still wired to the intent it claims.
12
- *
13
- * It registers a debug provider too. `registerGameDebug` is global and
14
- * last-wins, and only the running scene's flow script registers one — so
15
- * without this, everything a tool reads while the menu is up would come back
16
- * empty (or, worse, stale from the previous scene).
17
- */
18
- import { Script, registerGameDebug, type PropertySchema } from '@pix3/runtime';
19
-
20
- export class MenuFlow extends Script {
21
- private playButtonReady = false;
22
- private disposeDebug: (() => void) | null = null;
23
- private disposeCommands: (() => void)[] = [];
24
-
25
- constructor(id: string, type: string) {
26
- super(id, type);
27
- this.config = {
28
- gameScene: 'res://scenes/main.pix3scene',
29
- playButton: 'play-button',
30
- };
31
- }
32
-
33
- static getPropertySchema(): PropertySchema {
34
- const str = (name: string, label: string) => ({
35
- name,
36
- type: 'string' as const,
37
- ui: { label, group: 'Menu' },
38
- getValue: (s: unknown) => (s as MenuFlow).config[name],
39
- setValue: (s: unknown, v: unknown) => {
40
- (s as MenuFlow).config[name] = typeof v === 'string' ? v : '';
41
- },
42
- });
43
-
44
- return {
45
- nodeType: 'MenuFlow',
46
- properties: [str('gameScene', 'Game Scene'), str('playButton', 'Play Button')],
47
- groups: { Menu: { label: 'Menu Flow', expanded: true } },
48
- };
49
- }
50
-
51
- onStart(): void {
52
- const commands = this.scene?.commands;
53
- this.disposeCommands = [
54
- commands?.register('start-game', () => this.startGame(), {
55
- description: 'Leave the menu and transition into the game scene.',
56
- }),
57
- ].filter((dispose): dispose is () => void => dispose !== undefined);
58
-
59
- this.disposeDebug = registerGameDebug({
60
- name: 'recipe-grid-3d',
61
- version: 1,
62
- // The registry IS the action list — never a second, hand-kept copy.
63
- actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
64
- // `scene` is what tells a reader which flow answered: the game scene's
65
- // GameRules publishes under the same name.
66
- snapshot: () => ({
67
- scene: 'menu',
68
- gameScene: String(this.config.gameScene ?? ''),
69
- playButtonReady: this.playButtonReady,
70
- }),
71
- });
72
-
73
- const button = this.findNode(String(this.config.playButton ?? ''));
74
- if (!button) {
75
- console.warn(`[MenuFlow] Button "${this.config.playButton}" not found.`);
76
- return;
77
- }
78
- this.playButtonReady = true;
79
- button.connect('pressed', this, () => {
80
- this.scene?.commands.dispatch('start-game');
81
- });
82
- }
83
-
84
- onDetach(): void {
85
- // Safe in any order: the disposer clears the global only while it still
86
- // holds *this* provider, so the next scene's registration is never wiped.
87
- this.disposeDebug?.();
88
- this.disposeDebug = null;
89
- // The scene's registry is cleared on stop anyway; this covers the other
90
- // case — this script being detached while the scene keeps running.
91
- for (const dispose of this.disposeCommands) {
92
- dispose();
93
- }
94
- this.disposeCommands = [];
95
- this.playButtonReady = false;
96
- super.onDetach();
97
- }
98
-
99
- /** Transition into the game scene. */
100
- startGame(): void {
101
- void this.scene?.changeScene(String(this.config.gameScene ?? ''), {
102
- transition: 'fade',
103
- durationSec: 0.3,
104
- });
105
- }
106
- }
1
+ /**
2
+ * MenuFlow — drives the menu scene (the project's entry/export scene).
3
+ *
4
+ * PLAY transitions to the game scene with a fade. That is all it does: keep the
5
+ * menu dumb so the game scene stays directly playable on its own (agents and
6
+ * the editor open `main.pix3scene` and press play without going through here).
7
+ *
8
+ * PLAY does not call `startGame()` directly: it dispatches the command
9
+ * `start-game` (`scene.commands`). A test can then leave the menu without a tap,
10
+ * every raised intent lands in `scene.commands.log` with its frame, and one real
11
+ * tap is enough to prove the button is still wired to the intent it claims.
12
+ *
13
+ * It registers a debug provider too. `registerGameDebug` is global and
14
+ * last-wins, and only the running scene's flow script registers one — so
15
+ * without this, everything a tool reads while the menu is up would come back
16
+ * empty (or, worse, stale from the previous scene).
17
+ */
18
+ import { Script, registerGameDebug, type PropertySchema } from '@pix3/runtime';
19
+
20
+ export class MenuFlow extends Script {
21
+ private playButtonReady = false;
22
+ private disposeDebug: (() => void) | null = null;
23
+ private disposeCommands: (() => void)[] = [];
24
+
25
+ constructor(id: string, type: string) {
26
+ super(id, type);
27
+ this.config = {
28
+ gameScene: 'res://scenes/main.pix3scene',
29
+ playButton: 'play-button',
30
+ };
31
+ }
32
+
33
+ static getPropertySchema(): PropertySchema {
34
+ const str = (name: string, label: string) => ({
35
+ name,
36
+ type: 'string' as const,
37
+ ui: { label, group: 'Menu' },
38
+ getValue: (s: unknown) => (s as MenuFlow).config[name],
39
+ setValue: (s: unknown, v: unknown) => {
40
+ (s as MenuFlow).config[name] = typeof v === 'string' ? v : '';
41
+ },
42
+ });
43
+
44
+ return {
45
+ nodeType: 'MenuFlow',
46
+ properties: [str('gameScene', 'Game Scene'), str('playButton', 'Play Button')],
47
+ groups: { Menu: { label: 'Menu Flow', expanded: true } },
48
+ };
49
+ }
50
+
51
+ onStart(): void {
52
+ const commands = this.scene?.commands;
53
+ this.disposeCommands = [
54
+ commands?.register('start-game', () => this.startGame(), {
55
+ description: 'Leave the menu and transition into the game scene.',
56
+ }),
57
+ ].filter((dispose): dispose is () => void => dispose !== undefined);
58
+
59
+ this.disposeDebug = registerGameDebug({
60
+ name: 'recipe-grid-3d',
61
+ version: 1,
62
+ // The registry IS the action list — never a second, hand-kept copy.
63
+ actions: () => this.scene?.commands.list().map(command => command.name) ?? [],
64
+ // `scene` is what tells a reader which flow answered: the game scene's
65
+ // GameRules publishes under the same name.
66
+ snapshot: () => ({
67
+ scene: 'menu',
68
+ gameScene: String(this.config.gameScene ?? ''),
69
+ playButtonReady: this.playButtonReady,
70
+ }),
71
+ });
72
+
73
+ const button = this.findNode(String(this.config.playButton ?? ''));
74
+ if (!button) {
75
+ console.warn(`[MenuFlow] Button "${this.config.playButton}" not found.`);
76
+ return;
77
+ }
78
+ this.playButtonReady = true;
79
+ button.connect('pressed', this, () => {
80
+ this.scene?.commands.dispatch('start-game');
81
+ });
82
+ }
83
+
84
+ onDetach(): void {
85
+ // Safe in any order: the disposer clears the global only while it still
86
+ // holds *this* provider, so the next scene's registration is never wiped.
87
+ this.disposeDebug?.();
88
+ this.disposeDebug = null;
89
+ // The scene's registry is cleared on stop anyway; this covers the other
90
+ // case — this script being detached while the scene keeps running.
91
+ for (const dispose of this.disposeCommands) {
92
+ dispose();
93
+ }
94
+ this.disposeCommands = [];
95
+ this.playButtonReady = false;
96
+ super.onDetach();
97
+ }
98
+
99
+ /** Transition into the game scene. */
100
+ startGame(): void {
101
+ void this.scene?.changeScene(String(this.config.gameScene ?? ''), {
102
+ transition: 'fade',
103
+ durationSec: 0.3,
104
+ });
105
+ }
106
+ }
@@ -1,94 +1,94 @@
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
+ /**
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 +1,10 @@
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
+ 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 +1,39 @@
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
+ # 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
+ ```