@pix3/cli 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-7MZGPAUK.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-7WOFGLW3.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-HKG6YI6Y.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +32 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
@@ -1,192 +1,192 @@
1
- /**
2
- * TouchRules — turns a tap into meaning.
3
- *
4
- * Same role (and the same `rules` format) as in the other recipes; only the
5
- * source of the contact differs. Here a pointer-down is hit-tested against the
6
- * `core:Hitbox2D` groups listed in `rules` and the topmost match resolves.
7
- *
8
- * It emits the same semantic signals every recipe uses, on `game-root`:
9
- * `touch-scored` (amount, x, y)
10
- * `touch-damaged` (amount, x, y)
11
- *
12
- * `rules` format: `group:outcome:amount`, entries separated by `;`.
13
- * A tap that hits nothing can be punished too — see `missPenalty`.
14
- */
15
- import { Script, type PropertySchema } from '@pix3/runtime';
16
-
17
- type TouchOutcome = 'score' | 'damage';
18
-
19
- interface TouchRule {
20
- group: string;
21
- outcome: TouchOutcome;
22
- amount: number;
23
- }
24
-
25
- function parseRules(text: string): TouchRule[] {
26
- const rules: TouchRule[] = [];
27
- for (const entry of text.split(';')) {
28
- const parts = entry.trim().split(':');
29
- if (parts.length < 2 || !parts[0].trim()) {
30
- continue;
31
- }
32
- const amount = Number(parts[2] ?? 1);
33
- rules.push({
34
- group: parts[0].trim(),
35
- outcome: parts[1].trim() === 'damage' ? 'damage' : 'score',
36
- amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
37
- });
38
- }
39
- return rules;
40
- }
41
-
42
- export class TouchRules extends Script {
43
- constructor(id: string, type: string) {
44
- super(id, type);
45
- this.config = {
46
- // group:outcome:amount pairs, separated by ';'.
47
- rules: 'target:score:1;hazard:damage:1',
48
- // 0 = exact point test; larger = a more forgiving finger.
49
- tapRadius: 40,
50
- // Free the tapped node after resolving it.
51
- consume: true,
52
- // Damage dealt by a tap that hits nothing (0 = free misses).
53
- missPenalty: 0,
54
- scoreSound: '',
55
- damageSound: '',
56
- };
57
- }
58
-
59
- static getPropertySchema(): PropertySchema {
60
- const num = (name: string, label: string, min: number, max: number, step: number) => ({
61
- name,
62
- type: 'number' as const,
63
- ui: { label, group: 'Contact', min, max, step, slider: true },
64
- getValue: (s: unknown) => (s as TouchRules).config[name],
65
- setValue: (s: unknown, v: unknown) => {
66
- const n = Number(v);
67
- (s as TouchRules).config[name] = Math.min(max, Math.max(min, Number.isFinite(n) ? n : min));
68
- },
69
- });
70
- const str = (name: string, label: string, group: string, editor?: 'audio-resource') => ({
71
- name,
72
- type: 'string' as const,
73
- ui: { label, group, editor },
74
- getValue: (s: unknown) => (s as TouchRules).config[name],
75
- setValue: (s: unknown, v: unknown) => {
76
- (s as TouchRules).config[name] = typeof v === 'string' ? v : '';
77
- },
78
- });
79
-
80
- return {
81
- nodeType: 'TouchRules',
82
- properties: [
83
- str('rules', 'Rules', 'Contact'),
84
- num('tapRadius', 'Tap Radius', 0, 400, 1),
85
- num('missPenalty', 'Miss Penalty', 0, 10, 1),
86
- {
87
- name: 'consume',
88
- type: 'boolean',
89
- ui: { label: 'Consume On Tap', group: 'Contact' },
90
- getValue: s => (s as TouchRules).config.consume,
91
- setValue: (s, v) => {
92
- (s as TouchRules).config.consume = Boolean(v);
93
- },
94
- },
95
- str('scoreSound', 'Score Sound', 'Feedback', 'audio-resource'),
96
- str('damageSound', 'Damage Sound', 'Feedback', 'audio-resource'),
97
- ],
98
- groups: {
99
- Contact: { label: 'Contact', expanded: true },
100
- Feedback: { label: 'Feedback', expanded: false },
101
- },
102
- };
103
- }
104
-
105
- /**
106
- * Every finger that landed this frame resolves on its own.
107
- *
108
- * Both halves of that are addressed on purpose. The UI gate asks whether **this** finger is over
109
- * a control, not whether *any* control is hovered — the aggregate meant a thumb parked on a HUD
110
- * button silently swallowed taps made with the other hand. And the contact point comes from the
111
- * finger that produced the event, not from the shared pointer position, which under multi-touch
112
- * describes only the oldest finger.
113
- */
114
- onUpdate(): void {
115
- const input = this.input;
116
- if (!this.scene || !this.node || !input) {
117
- return;
118
- }
119
- for (const event of input.pointerEvents) {
120
- if (event.type !== 'down') {
121
- continue;
122
- }
123
- // With nothing in the addressed map — a harness driving the game by assigning input fields —
124
- // there is no per-pointer hover to read, so the aggregate is all there is.
125
- const overUI =
126
- input.pointerDownCount > 0 ? input.isPointerOverUI(event.pointerId) : input.isHoveringUI;
127
- if (overUI) {
128
- continue;
129
- }
130
- this.resolveTap(event.pointerId);
131
- }
132
- }
133
-
134
- /**
135
- * Resolve one contact: the topmost rule match wins, and a tap that hits nothing may be punished.
136
- */
137
- private resolveTap(pointerId: number): void {
138
- const scene = this.scene;
139
- const owner = this.node;
140
- if (!scene || !owner) {
141
- return;
142
- }
143
- // A tap that went down and up inside one frame is already out of the pointer map by the time
144
- // scripts run, so the addressed lookup misses; the shared position still holds where it landed.
145
- const pointer =
146
- scene.getPointer2DWorldPosition(pointerId) ?? scene.getPointer2DWorldPosition();
147
- if (!pointer) {
148
- return;
149
- }
150
-
151
- const radius = Math.max(0, Number(this.config.tapRadius) || 0);
152
- const consume = Boolean(this.config.consume);
153
-
154
- for (const rule of parseRules(String(this.config.rules ?? ''))) {
155
- const hits =
156
- radius > 0
157
- ? scene.collision2d.overlapCircle(pointer.x, pointer.y, radius, rule.group)
158
- : scene.collision2d.overlapPoint(pointer.x, pointer.y, rule.group);
159
- // Only the first match resolves: one tap is one action.
160
- const hit = hits[0];
161
- if (!hit) {
162
- continue;
163
- }
164
- if (rule.outcome === 'damage') {
165
- owner.emit('touch-damaged', rule.amount, hit.x, hit.y);
166
- scene.juice.flash({ color: '#ff4d6d', intensity: 0.4, durationSec: 0.22 });
167
- this.playSound(String(this.config.damageSound ?? ''));
168
- } else {
169
- owner.emit('touch-scored', rule.amount, hit.x, hit.y);
170
- scene.juice.punchScale(hit.node, { amount: 0.3, duration: 0.18 });
171
- this.playSound(String(this.config.scoreSound ?? ''));
172
- }
173
- if (consume) {
174
- hit.node.queueFree();
175
- }
176
- return;
177
- }
178
-
179
- const penalty = Math.max(0, Number(this.config.missPenalty) || 0);
180
- if (penalty > 0) {
181
- owner.emit('touch-damaged', penalty, pointer.x, pointer.y);
182
- this.playSound(String(this.config.damageSound ?? ''));
183
- }
184
- }
185
-
186
- private playSound(path: string): void {
187
- if (!path) {
188
- return;
189
- }
190
- void this.scene?.audio.play(path, { bus: 'sfx', pitchVariation: 0.08 });
191
- }
192
- }
1
+ /**
2
+ * TouchRules — turns a tap into meaning.
3
+ *
4
+ * Same role (and the same `rules` format) as in the other recipes; only the
5
+ * source of the contact differs. Here a pointer-down is hit-tested against the
6
+ * `core:Hitbox2D` groups listed in `rules` and the topmost match resolves.
7
+ *
8
+ * It emits the same semantic signals every recipe uses, on `game-root`:
9
+ * `touch-scored` (amount, x, y)
10
+ * `touch-damaged` (amount, x, y)
11
+ *
12
+ * `rules` format: `group:outcome:amount`, entries separated by `;`.
13
+ * A tap that hits nothing can be punished too — see `missPenalty`.
14
+ */
15
+ import { Script, type PropertySchema } from '@pix3/runtime';
16
+
17
+ type TouchOutcome = 'score' | 'damage';
18
+
19
+ interface TouchRule {
20
+ group: string;
21
+ outcome: TouchOutcome;
22
+ amount: number;
23
+ }
24
+
25
+ function parseRules(text: string): TouchRule[] {
26
+ const rules: TouchRule[] = [];
27
+ for (const entry of text.split(';')) {
28
+ const parts = entry.trim().split(':');
29
+ if (parts.length < 2 || !parts[0].trim()) {
30
+ continue;
31
+ }
32
+ const amount = Number(parts[2] ?? 1);
33
+ rules.push({
34
+ group: parts[0].trim(),
35
+ outcome: parts[1].trim() === 'damage' ? 'damage' : 'score',
36
+ amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
37
+ });
38
+ }
39
+ return rules;
40
+ }
41
+
42
+ export class TouchRules extends Script {
43
+ constructor(id: string, type: string) {
44
+ super(id, type);
45
+ this.config = {
46
+ // group:outcome:amount pairs, separated by ';'.
47
+ rules: 'target:score:1;hazard:damage:1',
48
+ // 0 = exact point test; larger = a more forgiving finger.
49
+ tapRadius: 40,
50
+ // Free the tapped node after resolving it.
51
+ consume: true,
52
+ // Damage dealt by a tap that hits nothing (0 = free misses).
53
+ missPenalty: 0,
54
+ scoreSound: '',
55
+ damageSound: '',
56
+ };
57
+ }
58
+
59
+ static getPropertySchema(): PropertySchema {
60
+ const num = (name: string, label: string, min: number, max: number, step: number) => ({
61
+ name,
62
+ type: 'number' as const,
63
+ ui: { label, group: 'Contact', min, max, step, slider: true },
64
+ getValue: (s: unknown) => (s as TouchRules).config[name],
65
+ setValue: (s: unknown, v: unknown) => {
66
+ const n = Number(v);
67
+ (s as TouchRules).config[name] = Math.min(max, Math.max(min, Number.isFinite(n) ? n : min));
68
+ },
69
+ });
70
+ const str = (name: string, label: string, group: string, editor?: 'audio-resource') => ({
71
+ name,
72
+ type: 'string' as const,
73
+ ui: { label, group, editor },
74
+ getValue: (s: unknown) => (s as TouchRules).config[name],
75
+ setValue: (s: unknown, v: unknown) => {
76
+ (s as TouchRules).config[name] = typeof v === 'string' ? v : '';
77
+ },
78
+ });
79
+
80
+ return {
81
+ nodeType: 'TouchRules',
82
+ properties: [
83
+ str('rules', 'Rules', 'Contact'),
84
+ num('tapRadius', 'Tap Radius', 0, 400, 1),
85
+ num('missPenalty', 'Miss Penalty', 0, 10, 1),
86
+ {
87
+ name: 'consume',
88
+ type: 'boolean',
89
+ ui: { label: 'Consume On Tap', group: 'Contact' },
90
+ getValue: s => (s as TouchRules).config.consume,
91
+ setValue: (s, v) => {
92
+ (s as TouchRules).config.consume = Boolean(v);
93
+ },
94
+ },
95
+ str('scoreSound', 'Score Sound', 'Feedback', 'audio-resource'),
96
+ str('damageSound', 'Damage Sound', 'Feedback', 'audio-resource'),
97
+ ],
98
+ groups: {
99
+ Contact: { label: 'Contact', expanded: true },
100
+ Feedback: { label: 'Feedback', expanded: false },
101
+ },
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Every finger that landed this frame resolves on its own.
107
+ *
108
+ * Both halves of that are addressed on purpose. The UI gate asks whether **this** finger is over
109
+ * a control, not whether *any* control is hovered — the aggregate meant a thumb parked on a HUD
110
+ * button silently swallowed taps made with the other hand. And the contact point comes from the
111
+ * finger that produced the event, not from the shared pointer position, which under multi-touch
112
+ * describes only the oldest finger.
113
+ */
114
+ onUpdate(): void {
115
+ const input = this.input;
116
+ if (!this.scene || !this.node || !input) {
117
+ return;
118
+ }
119
+ for (const event of input.pointerEvents) {
120
+ if (event.type !== 'down') {
121
+ continue;
122
+ }
123
+ // With nothing in the addressed map — a harness driving the game by assigning input fields —
124
+ // there is no per-pointer hover to read, so the aggregate is all there is.
125
+ const overUI =
126
+ input.pointerDownCount > 0 ? input.isPointerOverUI(event.pointerId) : input.isHoveringUI;
127
+ if (overUI) {
128
+ continue;
129
+ }
130
+ this.resolveTap(event.pointerId);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Resolve one contact: the topmost rule match wins, and a tap that hits nothing may be punished.
136
+ */
137
+ private resolveTap(pointerId: number): void {
138
+ const scene = this.scene;
139
+ const owner = this.node;
140
+ if (!scene || !owner) {
141
+ return;
142
+ }
143
+ // A tap that went down and up inside one frame is already out of the pointer map by the time
144
+ // scripts run, so the addressed lookup misses; the shared position still holds where it landed.
145
+ const pointer =
146
+ scene.getPointer2DWorldPosition(pointerId) ?? scene.getPointer2DWorldPosition();
147
+ if (!pointer) {
148
+ return;
149
+ }
150
+
151
+ const radius = Math.max(0, Number(this.config.tapRadius) || 0);
152
+ const consume = Boolean(this.config.consume);
153
+
154
+ for (const rule of parseRules(String(this.config.rules ?? ''))) {
155
+ const hits =
156
+ radius > 0
157
+ ? scene.collision2d.overlapCircle(pointer.x, pointer.y, radius, rule.group)
158
+ : scene.collision2d.overlapPoint(pointer.x, pointer.y, rule.group);
159
+ // Only the first match resolves: one tap is one action.
160
+ const hit = hits[0];
161
+ if (!hit) {
162
+ continue;
163
+ }
164
+ if (rule.outcome === 'damage') {
165
+ owner.emit('touch-damaged', rule.amount, hit.x, hit.y);
166
+ scene.juice.flash({ color: '#ff4d6d', intensity: 0.4, durationSec: 0.22 });
167
+ this.playSound(String(this.config.damageSound ?? ''));
168
+ } else {
169
+ owner.emit('touch-scored', rule.amount, hit.x, hit.y);
170
+ scene.juice.punchScale(hit.node, { amount: 0.3, duration: 0.18 });
171
+ this.playSound(String(this.config.scoreSound ?? ''));
172
+ }
173
+ if (consume) {
174
+ hit.node.queueFree();
175
+ }
176
+ return;
177
+ }
178
+
179
+ const penalty = Math.max(0, Number(this.config.missPenalty) || 0);
180
+ if (penalty > 0) {
181
+ owner.emit('touch-damaged', penalty, pointer.x, pointer.y);
182
+ this.playSound(String(this.config.damageSound ?? ''));
183
+ }
184
+ }
185
+
186
+ private playSound(path: string): void {
187
+ if (!path) {
188
+ return;
189
+ }
190
+ void this.scene?.audio.play(path, { bus: 'sfx', pitchVariation: 0.08 });
191
+ }
192
+ }
@@ -1,10 +1,10 @@
1
- id: recipe-tapper-2d
2
- title: Tapper 2D
3
- description: Objects fall into the field and tapping them is the whole game — good ones score, one bad type costs a life, and a countdown ends the run. Whack-a-mole, catch-the-falling, clickers.
4
- projectType: 2d
5
- targetPlatform: universal
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 game).
9
- entryScene: scenes/menu.pix3scene
10
- order: 62
1
+ id: recipe-tapper-2d
2
+ title: Tapper 2D
3
+ description: Objects fall into the field and tapping them is the whole game — good ones score, one bad type costs a life, and a countdown ends the run. Whack-a-mole, catch-the-falling, clickers.
4
+ projectType: 2d
5
+ targetPlatform: universal
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 game).
9
+ entryScene: scenes/menu.pix3scene
10
+ order: 62