@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.
- package/LICENSE +201 -201
- package/README.md +788 -754
- package/dist/index.js +208 -203
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-7MZGPAUK.mjs} +1 -1
- package/dist/smoke/prebuilt/chunk-7WOFGLW3.mjs +239 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/bundle-main.js +13 -13
- package/dist/validate/prebuilt/chunk-HKG6YI6Y.js +239 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
- package/kit/files/.claude/skills/pix3-nodes/reference.md +32 -24
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -28
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
- package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
- package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
- package/kit/files/AGENTS.md +203 -203
- package/kit/files/CLAUDE.md +1 -1
- package/kit/kit.json +2 -2
- package/package.json +54 -54
- package/templates/empty-2d/files/README.md +20 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
- package/templates/empty-2d/template.yaml +7 -7
- package/templates/empty-3d/files/README.md +20 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
- package/templates/empty-3d/template.yaml +7 -7
- package/templates/idea-blank/files/README.md +22 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
- package/templates/idea-blank/template.yaml +10 -10
- package/templates/minigame-2d/files/README.md +44 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
- package/templates/minigame-2d/template.yaml +10 -10
- package/templates/playable-2d/files/README.md +30 -30
- package/templates/playable-2d/files/design/recipe.md +83 -83
- package/templates/playable-2d/files/design/tests/reachability.json +4 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
- package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
- package/templates/playable-2d/template.yaml +9 -9
- package/templates/playable-3d/files/README.md +32 -32
- package/templates/playable-3d/files/design/recipe.md +127 -127
- package/templates/playable-3d/files/design/tests/reachability.json +4 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
- package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
- package/templates/playable-3d/template.yaml +9 -9
- package/templates/recipe-arena-2d/files/README.md +48 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
- package/templates/recipe-arena-2d/template.yaml +10 -10
- package/templates/recipe-blank-2d/files/README.md +61 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
- package/templates/recipe-blank-2d/template.yaml +10 -10
- package/templates/recipe-bouncer-2d/files/README.md +81 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
- package/templates/recipe-bouncer-2d/template.yaml +10 -10
- package/templates/recipe-grid-3d/files/README.md +22 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-grid-3d/template.yaml +10 -10
- package/templates/recipe-tapper-2d/files/README.md +39 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
- package/templates/recipe-tapper-2d/template.yaml +10 -10
- package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
- 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
|