@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,174 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TouchRules — turns contact into meaning.
|
|
3
|
-
*
|
|
4
|
-
* Every frame it overlap-tests a circle around the player against the
|
|
5
|
-
* `core:Hitbox2D` groups listed in `rules`, and for each hit emits a SEMANTIC
|
|
6
|
-
* signal on its own node (`game-root`):
|
|
7
|
-
*
|
|
8
|
-
* `touch-scored` (amount, x, y)
|
|
9
|
-
* `touch-damaged` (amount, x, y)
|
|
10
|
-
*
|
|
11
|
-
* It never touches the score, the lives or the HUD — `GameRules` owns those and
|
|
12
|
-
* listens for these two signals. Add an outcome by adding a rule here and a
|
|
13
|
-
* listener there; nothing else has to change.
|
|
14
|
-
*
|
|
15
|
-
* `rules` format: `group:outcome:amount`, entries separated by `;`.
|
|
16
|
-
* Outcomes: `score` | `damage`. Example: `pickup:score:1;hazard:damage:1`.
|
|
17
|
-
*/
|
|
18
|
-
import { Script, type PropertySchema } from '@pix3/runtime';
|
|
19
|
-
|
|
20
|
-
type TouchOutcome = 'score' | 'damage';
|
|
21
|
-
|
|
22
|
-
interface TouchRule {
|
|
23
|
-
group: string;
|
|
24
|
-
outcome: TouchOutcome;
|
|
25
|
-
amount: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function parseRules(text: string): TouchRule[] {
|
|
29
|
-
const rules: TouchRule[] = [];
|
|
30
|
-
for (const entry of text.split(';')) {
|
|
31
|
-
const parts = entry.trim().split(':');
|
|
32
|
-
if (parts.length < 2 || !parts[0].trim()) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const outcome: TouchOutcome = parts[1].trim() === 'damage' ? 'damage' : 'score';
|
|
36
|
-
const amount = Number(parts[2] ?? 1);
|
|
37
|
-
rules.push({
|
|
38
|
-
group: parts[0].trim(),
|
|
39
|
-
outcome,
|
|
40
|
-
amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return rules;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class TouchRules extends Script {
|
|
47
|
-
constructor(id: string, type: string) {
|
|
48
|
-
super(id, type);
|
|
49
|
-
this.config = {
|
|
50
|
-
// Node id/name whose world position is the centre of the overlap test.
|
|
51
|
-
playerNode: 'player',
|
|
52
|
-
// Radius of the overlap test, in logical pixels.
|
|
53
|
-
touchRadius: 62,
|
|
54
|
-
// group:outcome:amount pairs, separated by ';'.
|
|
55
|
-
rules: 'pickup:score:1;hazard:damage:1',
|
|
56
|
-
// Free the touched node after resolving it.
|
|
57
|
-
consume: true,
|
|
58
|
-
// res:// audio paths; empty = silent.
|
|
59
|
-
scoreSound: '',
|
|
60
|
-
damageSound: '',
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
static getPropertySchema(): PropertySchema {
|
|
65
|
-
return {
|
|
66
|
-
nodeType: 'TouchRules',
|
|
67
|
-
properties: [
|
|
68
|
-
{
|
|
69
|
-
name: 'playerNode',
|
|
70
|
-
type: 'string',
|
|
71
|
-
ui: { label: 'Player Node', group: 'Contact' },
|
|
72
|
-
getValue: s => (s as TouchRules).config.playerNode,
|
|
73
|
-
setValue: (s, v) => {
|
|
74
|
-
(s as TouchRules).config.playerNode = typeof v === 'string' ? v : '';
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: 'touchRadius',
|
|
79
|
-
type: 'number',
|
|
80
|
-
ui: { label: 'Touch Radius', group: 'Contact', min: 4, max: 600, step: 1, slider: true },
|
|
81
|
-
getValue: s => (s as TouchRules).config.touchRadius,
|
|
82
|
-
setValue: (s, v) => {
|
|
83
|
-
const n = Number(v);
|
|
84
|
-
(s as TouchRules).config.touchRadius = Math.min(600, Math.max(4, Number.isFinite(n) ? n : 4));
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'rules',
|
|
89
|
-
type: 'string',
|
|
90
|
-
ui: {
|
|
91
|
-
label: 'Rules',
|
|
92
|
-
description: "group:outcome:amount, ';'-separated. Outcome is score or damage.",
|
|
93
|
-
group: 'Contact',
|
|
94
|
-
},
|
|
95
|
-
getValue: s => (s as TouchRules).config.rules,
|
|
96
|
-
setValue: (s, v) => {
|
|
97
|
-
(s as TouchRules).config.rules = typeof v === 'string' ? v : '';
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: 'consume',
|
|
102
|
-
type: 'boolean',
|
|
103
|
-
ui: { label: 'Consume On Touch', group: 'Contact' },
|
|
104
|
-
getValue: s => (s as TouchRules).config.consume,
|
|
105
|
-
setValue: (s, v) => {
|
|
106
|
-
(s as TouchRules).config.consume = Boolean(v);
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: 'scoreSound',
|
|
111
|
-
type: 'string',
|
|
112
|
-
ui: { label: 'Score Sound', group: 'Feedback', editor: 'audio-resource' },
|
|
113
|
-
getValue: s => (s as TouchRules).config.scoreSound,
|
|
114
|
-
setValue: (s, v) => {
|
|
115
|
-
(s as TouchRules).config.scoreSound = typeof v === 'string' ? v : '';
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: 'damageSound',
|
|
120
|
-
type: 'string',
|
|
121
|
-
ui: { label: 'Damage Sound', group: 'Feedback', editor: 'audio-resource' },
|
|
122
|
-
getValue: s => (s as TouchRules).config.damageSound,
|
|
123
|
-
setValue: (s, v) => {
|
|
124
|
-
(s as TouchRules).config.damageSound = typeof v === 'string' ? v : '';
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
groups: {
|
|
129
|
-
Contact: { label: 'Contact', expanded: true },
|
|
130
|
-
Feedback: { label: 'Feedback', expanded: false },
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
onUpdate(): void {
|
|
136
|
-
const scene = this.scene;
|
|
137
|
-
const owner = this.node;
|
|
138
|
-
const player = this.findNode(String(this.config.playerNode ?? ''));
|
|
139
|
-
if (!scene || !owner || !player) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
player.updateWorldMatrix(true, false);
|
|
144
|
-
const px = player.matrixWorld.elements[12];
|
|
145
|
-
const py = player.matrixWorld.elements[13];
|
|
146
|
-
const radius = Math.max(1, Number(this.config.touchRadius) || 1);
|
|
147
|
-
const consume = Boolean(this.config.consume);
|
|
148
|
-
|
|
149
|
-
for (const rule of parseRules(String(this.config.rules ?? ''))) {
|
|
150
|
-
for (const hit of scene.collision2d.overlapCircle(px, py, radius, rule.group)) {
|
|
151
|
-
if (rule.outcome === 'damage') {
|
|
152
|
-
owner.emit('touch-damaged', rule.amount, hit.x, hit.y);
|
|
153
|
-
scene.juice.shake(player, { amplitude: 16, duration: 0.25 });
|
|
154
|
-
scene.juice.flash({ color: '#ff4d6d', intensity: 0.4, durationSec: 0.22 });
|
|
155
|
-
this.playSound(String(this.config.damageSound ?? ''));
|
|
156
|
-
} else {
|
|
157
|
-
owner.emit('touch-scored', rule.amount, hit.x, hit.y);
|
|
158
|
-
scene.juice.punchScale(player, { amount: 0.22, duration: 0.22 });
|
|
159
|
-
this.playSound(String(this.config.scoreSound ?? ''));
|
|
160
|
-
}
|
|
161
|
-
if (consume) {
|
|
162
|
-
hit.node.queueFree();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private playSound(path: string): void {
|
|
169
|
-
if (!path) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
void this.scene?.audio.play(path, { bus: 'sfx', pitchVariation: 0.08 });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* TouchRules — turns contact into meaning.
|
|
3
|
+
*
|
|
4
|
+
* Every frame it overlap-tests a circle around the player against the
|
|
5
|
+
* `core:Hitbox2D` groups listed in `rules`, and for each hit emits a SEMANTIC
|
|
6
|
+
* signal on its own node (`game-root`):
|
|
7
|
+
*
|
|
8
|
+
* `touch-scored` (amount, x, y)
|
|
9
|
+
* `touch-damaged` (amount, x, y)
|
|
10
|
+
*
|
|
11
|
+
* It never touches the score, the lives or the HUD — `GameRules` owns those and
|
|
12
|
+
* listens for these two signals. Add an outcome by adding a rule here and a
|
|
13
|
+
* listener there; nothing else has to change.
|
|
14
|
+
*
|
|
15
|
+
* `rules` format: `group:outcome:amount`, entries separated by `;`.
|
|
16
|
+
* Outcomes: `score` | `damage`. Example: `pickup:score:1;hazard:damage:1`.
|
|
17
|
+
*/
|
|
18
|
+
import { Script, type PropertySchema } from '@pix3/runtime';
|
|
19
|
+
|
|
20
|
+
type TouchOutcome = 'score' | 'damage';
|
|
21
|
+
|
|
22
|
+
interface TouchRule {
|
|
23
|
+
group: string;
|
|
24
|
+
outcome: TouchOutcome;
|
|
25
|
+
amount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseRules(text: string): TouchRule[] {
|
|
29
|
+
const rules: TouchRule[] = [];
|
|
30
|
+
for (const entry of text.split(';')) {
|
|
31
|
+
const parts = entry.trim().split(':');
|
|
32
|
+
if (parts.length < 2 || !parts[0].trim()) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const outcome: TouchOutcome = parts[1].trim() === 'damage' ? 'damage' : 'score';
|
|
36
|
+
const amount = Number(parts[2] ?? 1);
|
|
37
|
+
rules.push({
|
|
38
|
+
group: parts[0].trim(),
|
|
39
|
+
outcome,
|
|
40
|
+
amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return rules;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class TouchRules extends Script {
|
|
47
|
+
constructor(id: string, type: string) {
|
|
48
|
+
super(id, type);
|
|
49
|
+
this.config = {
|
|
50
|
+
// Node id/name whose world position is the centre of the overlap test.
|
|
51
|
+
playerNode: 'player',
|
|
52
|
+
// Radius of the overlap test, in logical pixels.
|
|
53
|
+
touchRadius: 62,
|
|
54
|
+
// group:outcome:amount pairs, separated by ';'.
|
|
55
|
+
rules: 'pickup:score:1;hazard:damage:1',
|
|
56
|
+
// Free the touched node after resolving it.
|
|
57
|
+
consume: true,
|
|
58
|
+
// res:// audio paths; empty = silent.
|
|
59
|
+
scoreSound: '',
|
|
60
|
+
damageSound: '',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static getPropertySchema(): PropertySchema {
|
|
65
|
+
return {
|
|
66
|
+
nodeType: 'TouchRules',
|
|
67
|
+
properties: [
|
|
68
|
+
{
|
|
69
|
+
name: 'playerNode',
|
|
70
|
+
type: 'string',
|
|
71
|
+
ui: { label: 'Player Node', group: 'Contact' },
|
|
72
|
+
getValue: s => (s as TouchRules).config.playerNode,
|
|
73
|
+
setValue: (s, v) => {
|
|
74
|
+
(s as TouchRules).config.playerNode = typeof v === 'string' ? v : '';
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'touchRadius',
|
|
79
|
+
type: 'number',
|
|
80
|
+
ui: { label: 'Touch Radius', group: 'Contact', min: 4, max: 600, step: 1, slider: true },
|
|
81
|
+
getValue: s => (s as TouchRules).config.touchRadius,
|
|
82
|
+
setValue: (s, v) => {
|
|
83
|
+
const n = Number(v);
|
|
84
|
+
(s as TouchRules).config.touchRadius = Math.min(600, Math.max(4, Number.isFinite(n) ? n : 4));
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'rules',
|
|
89
|
+
type: 'string',
|
|
90
|
+
ui: {
|
|
91
|
+
label: 'Rules',
|
|
92
|
+
description: "group:outcome:amount, ';'-separated. Outcome is score or damage.",
|
|
93
|
+
group: 'Contact',
|
|
94
|
+
},
|
|
95
|
+
getValue: s => (s as TouchRules).config.rules,
|
|
96
|
+
setValue: (s, v) => {
|
|
97
|
+
(s as TouchRules).config.rules = typeof v === 'string' ? v : '';
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'consume',
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
ui: { label: 'Consume On Touch', group: 'Contact' },
|
|
104
|
+
getValue: s => (s as TouchRules).config.consume,
|
|
105
|
+
setValue: (s, v) => {
|
|
106
|
+
(s as TouchRules).config.consume = Boolean(v);
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'scoreSound',
|
|
111
|
+
type: 'string',
|
|
112
|
+
ui: { label: 'Score Sound', group: 'Feedback', editor: 'audio-resource' },
|
|
113
|
+
getValue: s => (s as TouchRules).config.scoreSound,
|
|
114
|
+
setValue: (s, v) => {
|
|
115
|
+
(s as TouchRules).config.scoreSound = typeof v === 'string' ? v : '';
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'damageSound',
|
|
120
|
+
type: 'string',
|
|
121
|
+
ui: { label: 'Damage Sound', group: 'Feedback', editor: 'audio-resource' },
|
|
122
|
+
getValue: s => (s as TouchRules).config.damageSound,
|
|
123
|
+
setValue: (s, v) => {
|
|
124
|
+
(s as TouchRules).config.damageSound = typeof v === 'string' ? v : '';
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
groups: {
|
|
129
|
+
Contact: { label: 'Contact', expanded: true },
|
|
130
|
+
Feedback: { label: 'Feedback', expanded: false },
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onUpdate(): void {
|
|
136
|
+
const scene = this.scene;
|
|
137
|
+
const owner = this.node;
|
|
138
|
+
const player = this.findNode(String(this.config.playerNode ?? ''));
|
|
139
|
+
if (!scene || !owner || !player) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
player.updateWorldMatrix(true, false);
|
|
144
|
+
const px = player.matrixWorld.elements[12];
|
|
145
|
+
const py = player.matrixWorld.elements[13];
|
|
146
|
+
const radius = Math.max(1, Number(this.config.touchRadius) || 1);
|
|
147
|
+
const consume = Boolean(this.config.consume);
|
|
148
|
+
|
|
149
|
+
for (const rule of parseRules(String(this.config.rules ?? ''))) {
|
|
150
|
+
for (const hit of scene.collision2d.overlapCircle(px, py, radius, rule.group)) {
|
|
151
|
+
if (rule.outcome === 'damage') {
|
|
152
|
+
owner.emit('touch-damaged', rule.amount, hit.x, hit.y);
|
|
153
|
+
scene.juice.shake(player, { amplitude: 16, duration: 0.25 });
|
|
154
|
+
scene.juice.flash({ color: '#ff4d6d', intensity: 0.4, durationSec: 0.22 });
|
|
155
|
+
this.playSound(String(this.config.damageSound ?? ''));
|
|
156
|
+
} else {
|
|
157
|
+
owner.emit('touch-scored', rule.amount, hit.x, hit.y);
|
|
158
|
+
scene.juice.punchScale(player, { amount: 0.22, duration: 0.22 });
|
|
159
|
+
this.playSound(String(this.config.scoreSound ?? ''));
|
|
160
|
+
}
|
|
161
|
+
if (consume) {
|
|
162
|
+
hit.node.queueFree();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private playSound(path: string): void {
|
|
169
|
+
if (!path) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
void this.scene?.audio.play(path, { bus: 'sfx', pitchVariation: 0.08 });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
id: recipe-arena-2d
|
|
2
|
-
title: Arena 2D
|
|
3
|
-
description: An avatar you steer inside a bounded field while spawners drip pickups and hazards; touch turns into score or damage, with lives, a timer and a win/lose flow. Dodgers, collectors, top-down survival, and (by swapping the movement script) runners. Its steering is continuous, so a grid or turn-based game (snake, sokoban) starts from Blank 2D instead.
|
|
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: 60
|
|
1
|
+
id: recipe-arena-2d
|
|
2
|
+
title: Arena 2D
|
|
3
|
+
description: An avatar you steer inside a bounded field while spawners drip pickups and hazards; touch turns into score or damage, with lives, a timer and a win/lose flow. Dodgers, collectors, top-down survival, and (by swapping the movement script) runners. Its steering is continuous, so a grid or turn-based game (snake, sokoban) starts from Blank 2D instead.
|
|
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: 60
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
# Blank 2D
|
|
2
|
-
|
|
3
|
-
Everything a run needs **except the mechanic**: a bounded field, a HUD, score /
|
|
4
|
-
lives / timer bookkeeping, and a win/lose screen with RETRY. Press play in
|
|
5
|
-
`scenes/main.pix3scene` and you get an empty field with `SCORE 0` — that is
|
|
6
|
-
correct, and it is the point.
|
|
7
|
-
|
|
8
|
-
Use this when the game's core loop is not what another recipe ships (grid or
|
|
9
|
-
turn-based movement, word / card / board games, builders, idle). The first thing
|
|
10
|
-
to build is the mechanic itself, controls included; building it here beats
|
|
11
|
-
deleting somebody else's mechanic first. A falling, bouncing ball is the bouncer
|
|
12
|
-
recipe's job — do not rebuild that here.
|
|
13
|
-
|
|
14
|
-
The field is not grey: `post-fx` blooms anything bright and `bg-glow` lights the
|
|
15
|
-
background, and `sprites/ph-*.png` is a shape library (disc, orb, ring, star,
|
|
16
|
-
capsule) tinted with the palette at T0. Build the mechanic out of those shapes,
|
|
17
|
-
not out of `ColorRect2D` squares — see `design/recipe.md` → Placeholders.
|
|
18
|
-
|
|
19
|
-
**`design/recipe.md` is the contract.** It lists the stable node ids, the
|
|
20
|
-
tunables (with their ranges), the extension points and what must not be renamed.
|
|
21
|
-
Read it first; tools and agents grep it.
|
|
22
|
-
|
|
23
|
-
## Layout
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
scenes/main.pix3scene the game — open and build here. There is no menu on purpose.
|
|
27
|
-
scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
|
|
28
|
-
scripts/GameRules.ts score / lives / timer / win / lose / end flow / restart
|
|
29
|
-
scripts/ScoreHud.ts signals → HUD widgets (display only)
|
|
30
|
-
sprites/ph-*.png shape library (near-white, tinted via core:tint)
|
|
31
|
-
design/recipe.md the contract
|
|
32
|
-
design/tests/ reachability ledger + one example routine (a format seed)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## How your mechanic plugs in
|
|
36
|
-
|
|
37
|
-
Write a script, attach it to a node inside `board`, and score through
|
|
38
|
-
`GameRules` — either by emitting on `game-root`:
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
this.findNode('game-root')?.emit('score-added', 1);
|
|
42
|
-
this.findNode('game-root')?.emit('life-lost', 1);
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
…or by calling the component directly (`addScore`, `loseLife`, `finish`). The
|
|
46
|
-
HUD, the end screen and RETRY then work with no further wiring.
|
|
47
|
-
|
|
48
|
-
## The rule that keeps it workable
|
|
49
|
-
|
|
50
|
-
One file, one responsibility. Keep each script in the 70–140 line range: small
|
|
51
|
-
files can be edited surgically; monoliths get rewritten wholesale and lose the
|
|
52
|
-
comments that explained them. When a mechanic outgrows its file, split it rather
|
|
53
|
-
than growing it.
|
|
54
|
-
|
|
55
|
-
## No menu yet
|
|
56
|
-
|
|
57
|
-
A menu is a screen between you and the thing you are working on. Add one only
|
|
58
|
-
once the game is fun: create `scenes/menu.pix3scene`, have its PLAY button
|
|
59
|
-
`scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`, then
|
|
60
|
-
point Project Settings → Default Export Scene Path at it. `Play Scene` keeps
|
|
61
|
-
running whatever scene is open, so the menu never gets in the way again.
|
|
1
|
+
# Blank 2D
|
|
2
|
+
|
|
3
|
+
Everything a run needs **except the mechanic**: a bounded field, a HUD, score /
|
|
4
|
+
lives / timer bookkeeping, and a win/lose screen with RETRY. Press play in
|
|
5
|
+
`scenes/main.pix3scene` and you get an empty field with `SCORE 0` — that is
|
|
6
|
+
correct, and it is the point.
|
|
7
|
+
|
|
8
|
+
Use this when the game's core loop is not what another recipe ships (grid or
|
|
9
|
+
turn-based movement, word / card / board games, builders, idle). The first thing
|
|
10
|
+
to build is the mechanic itself, controls included; building it here beats
|
|
11
|
+
deleting somebody else's mechanic first. A falling, bouncing ball is the bouncer
|
|
12
|
+
recipe's job — do not rebuild that here.
|
|
13
|
+
|
|
14
|
+
The field is not grey: `post-fx` blooms anything bright and `bg-glow` lights the
|
|
15
|
+
background, and `sprites/ph-*.png` is a shape library (disc, orb, ring, star,
|
|
16
|
+
capsule) tinted with the palette at T0. Build the mechanic out of those shapes,
|
|
17
|
+
not out of `ColorRect2D` squares — see `design/recipe.md` → Placeholders.
|
|
18
|
+
|
|
19
|
+
**`design/recipe.md` is the contract.** It lists the stable node ids, the
|
|
20
|
+
tunables (with their ranges), the extension points and what must not be renamed.
|
|
21
|
+
Read it first; tools and agents grep it.
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
scenes/main.pix3scene the game — open and build here. There is no menu on purpose.
|
|
27
|
+
scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
|
|
28
|
+
scripts/GameRules.ts score / lives / timer / win / lose / end flow / restart
|
|
29
|
+
scripts/ScoreHud.ts signals → HUD widgets (display only)
|
|
30
|
+
sprites/ph-*.png shape library (near-white, tinted via core:tint)
|
|
31
|
+
design/recipe.md the contract
|
|
32
|
+
design/tests/ reachability ledger + one example routine (a format seed)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How your mechanic plugs in
|
|
36
|
+
|
|
37
|
+
Write a script, attach it to a node inside `board`, and score through
|
|
38
|
+
`GameRules` — either by emitting on `game-root`:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
this.findNode('game-root')?.emit('score-added', 1);
|
|
42
|
+
this.findNode('game-root')?.emit('life-lost', 1);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
…or by calling the component directly (`addScore`, `loseLife`, `finish`). The
|
|
46
|
+
HUD, the end screen and RETRY then work with no further wiring.
|
|
47
|
+
|
|
48
|
+
## The rule that keeps it workable
|
|
49
|
+
|
|
50
|
+
One file, one responsibility. Keep each script in the 70–140 line range: small
|
|
51
|
+
files can be edited surgically; monoliths get rewritten wholesale and lose the
|
|
52
|
+
comments that explained them. When a mechanic outgrows its file, split it rather
|
|
53
|
+
than growing it.
|
|
54
|
+
|
|
55
|
+
## No menu yet
|
|
56
|
+
|
|
57
|
+
A menu is a screen between you and the thing you are working on. Add one only
|
|
58
|
+
once the game is fun: create `scenes/menu.pix3scene`, have its PLAY button
|
|
59
|
+
`scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`, then
|
|
60
|
+
point Project Settings → Default Export Scene Path at it. `Play Scene` keeps
|
|
61
|
+
running whatever scene is open, so the menu never gets in the way again.
|