incanto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { f as Node } from "./loader-CGs_G-r0.js";
|
|
2
|
+
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
|
+
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
4
|
+
import { l as registerNode } from "./registry-BVJ2HbCn.js";
|
|
5
|
+
//#region src/core/audio/sfx-presets.ts
|
|
6
|
+
/**
|
|
7
|
+
* A tasteful, distinct set of common game sounds. Pick one via
|
|
8
|
+
* `AudioPlayer.preset`. Tuned by ear for crisp arcade feel; `seed`/`pitch`
|
|
9
|
+
* props add free variation so repeated sounds don't feel robotic.
|
|
10
|
+
*/
|
|
11
|
+
const SFX_PRESETS = {
|
|
12
|
+
coin: {
|
|
13
|
+
wave: "square",
|
|
14
|
+
baseFreq: 988,
|
|
15
|
+
freqRamp: 1800,
|
|
16
|
+
attack: 0,
|
|
17
|
+
sustain: .04,
|
|
18
|
+
decay: .18,
|
|
19
|
+
duty: .5,
|
|
20
|
+
volume: .4
|
|
21
|
+
},
|
|
22
|
+
pickup: {
|
|
23
|
+
wave: "sine",
|
|
24
|
+
baseFreq: 660,
|
|
25
|
+
freqRamp: 900,
|
|
26
|
+
attack: .005,
|
|
27
|
+
sustain: .05,
|
|
28
|
+
decay: .12,
|
|
29
|
+
volume: .45
|
|
30
|
+
},
|
|
31
|
+
jump: {
|
|
32
|
+
wave: "square",
|
|
33
|
+
baseFreq: 320,
|
|
34
|
+
freqRamp: 600,
|
|
35
|
+
attack: 0,
|
|
36
|
+
sustain: .03,
|
|
37
|
+
decay: .12,
|
|
38
|
+
duty: .35,
|
|
39
|
+
volume: .4
|
|
40
|
+
},
|
|
41
|
+
hurt: {
|
|
42
|
+
wave: "sawtooth",
|
|
43
|
+
baseFreq: 380,
|
|
44
|
+
freqRamp: -520,
|
|
45
|
+
attack: 0,
|
|
46
|
+
sustain: .05,
|
|
47
|
+
decay: .16,
|
|
48
|
+
volume: .5
|
|
49
|
+
},
|
|
50
|
+
hit: {
|
|
51
|
+
wave: "noise",
|
|
52
|
+
baseFreq: 600,
|
|
53
|
+
freqRamp: -400,
|
|
54
|
+
attack: 0,
|
|
55
|
+
sustain: .02,
|
|
56
|
+
decay: .1,
|
|
57
|
+
volume: .55
|
|
58
|
+
},
|
|
59
|
+
explosion: {
|
|
60
|
+
wave: "noise",
|
|
61
|
+
baseFreq: 420,
|
|
62
|
+
freqRamp: -360,
|
|
63
|
+
attack: 0,
|
|
64
|
+
sustain: .12,
|
|
65
|
+
decay: .5,
|
|
66
|
+
volume: .6
|
|
67
|
+
},
|
|
68
|
+
powerup: {
|
|
69
|
+
wave: "square",
|
|
70
|
+
baseFreq: 440,
|
|
71
|
+
freqRamp: 1200,
|
|
72
|
+
attack: .01,
|
|
73
|
+
sustain: .18,
|
|
74
|
+
decay: .22,
|
|
75
|
+
duty: .5,
|
|
76
|
+
vibratoDepth: .08,
|
|
77
|
+
vibratoRate: 18,
|
|
78
|
+
volume: .4
|
|
79
|
+
},
|
|
80
|
+
laser: {
|
|
81
|
+
wave: "sawtooth",
|
|
82
|
+
baseFreq: 1400,
|
|
83
|
+
freqRamp: -2600,
|
|
84
|
+
attack: 0,
|
|
85
|
+
sustain: .03,
|
|
86
|
+
decay: .14,
|
|
87
|
+
volume: .4
|
|
88
|
+
},
|
|
89
|
+
shoot: {
|
|
90
|
+
wave: "square",
|
|
91
|
+
baseFreq: 900,
|
|
92
|
+
freqRamp: -1400,
|
|
93
|
+
attack: 0,
|
|
94
|
+
sustain: .02,
|
|
95
|
+
decay: .1,
|
|
96
|
+
duty: .25,
|
|
97
|
+
volume: .45
|
|
98
|
+
},
|
|
99
|
+
blip: {
|
|
100
|
+
wave: "square",
|
|
101
|
+
baseFreq: 760,
|
|
102
|
+
freqRamp: 0,
|
|
103
|
+
attack: 0,
|
|
104
|
+
sustain: .015,
|
|
105
|
+
decay: .04,
|
|
106
|
+
duty: .5,
|
|
107
|
+
volume: .35
|
|
108
|
+
},
|
|
109
|
+
select: {
|
|
110
|
+
wave: "triangle",
|
|
111
|
+
baseFreq: 520,
|
|
112
|
+
freqRamp: 420,
|
|
113
|
+
attack: .002,
|
|
114
|
+
sustain: .04,
|
|
115
|
+
decay: .08,
|
|
116
|
+
volume: .4
|
|
117
|
+
},
|
|
118
|
+
step: {
|
|
119
|
+
wave: "noise",
|
|
120
|
+
baseFreq: 220,
|
|
121
|
+
freqRamp: -120,
|
|
122
|
+
attack: 0,
|
|
123
|
+
sustain: .01,
|
|
124
|
+
decay: .05,
|
|
125
|
+
volume: .3
|
|
126
|
+
},
|
|
127
|
+
win: {
|
|
128
|
+
wave: "square",
|
|
129
|
+
baseFreq: 523,
|
|
130
|
+
freqRamp: 900,
|
|
131
|
+
attack: .01,
|
|
132
|
+
sustain: .35,
|
|
133
|
+
decay: .3,
|
|
134
|
+
duty: .5,
|
|
135
|
+
vibratoDepth: .05,
|
|
136
|
+
vibratoRate: 10,
|
|
137
|
+
volume: .42
|
|
138
|
+
},
|
|
139
|
+
lose: {
|
|
140
|
+
wave: "sawtooth",
|
|
141
|
+
baseFreq: 392,
|
|
142
|
+
freqRamp: -300,
|
|
143
|
+
attack: .01,
|
|
144
|
+
sustain: .3,
|
|
145
|
+
decay: .4,
|
|
146
|
+
volume: .45
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const SFX_PRESET_NAMES = Object.keys(SFX_PRESETS);
|
|
150
|
+
const TWO_PI = Math.PI * 2;
|
|
151
|
+
/**
|
|
152
|
+
* Render a preset to mono PCM in [-1, 1]. PURE: identical inputs → identical
|
|
153
|
+
* output (the only randomness is the seeded noise generator), so determinism is
|
|
154
|
+
* unit-testable headlessly. Length = ceil((attack+sustain+decay) * sampleRate).
|
|
155
|
+
*/
|
|
156
|
+
function synthSfx(params, opts = {}) {
|
|
157
|
+
const sampleRate = opts.sampleRate ?? 44100;
|
|
158
|
+
const pitch = opts.pitch ?? 1;
|
|
159
|
+
const rng = new Rng((opts.seed ?? 0) ^ 42405);
|
|
160
|
+
const total = params.attack + params.sustain + params.decay;
|
|
161
|
+
const length = Math.max(1, Math.ceil(total * sampleRate));
|
|
162
|
+
const out = new Float32Array(length);
|
|
163
|
+
const duty = params.duty ?? .5;
|
|
164
|
+
const vibDepth = params.vibratoDepth ?? 0;
|
|
165
|
+
const vibRate = params.vibratoRate ?? 0;
|
|
166
|
+
let phase = 0;
|
|
167
|
+
let lastNoise = rng.range(-1, 1);
|
|
168
|
+
let noiseHold = 0;
|
|
169
|
+
for (let i = 0; i < length; i++) {
|
|
170
|
+
const t = i / sampleRate;
|
|
171
|
+
let env;
|
|
172
|
+
if (t < params.attack) env = params.attack > 0 ? t / params.attack : 1;
|
|
173
|
+
else if (t < params.attack + params.sustain) env = 1;
|
|
174
|
+
else {
|
|
175
|
+
const d = t - params.attack - params.sustain;
|
|
176
|
+
env = params.decay > 0 ? Math.max(0, 1 - d / params.decay) : 0;
|
|
177
|
+
}
|
|
178
|
+
let freq = (params.baseFreq + params.freqRamp * t) * pitch;
|
|
179
|
+
if (vibDepth > 0 && vibRate > 0) freq += freq * vibDepth * Math.sin(TWO_PI * vibRate * t);
|
|
180
|
+
freq = Math.max(0, freq);
|
|
181
|
+
phase += freq / sampleRate;
|
|
182
|
+
phase -= Math.floor(phase);
|
|
183
|
+
let sample;
|
|
184
|
+
switch (params.wave) {
|
|
185
|
+
case "square":
|
|
186
|
+
sample = phase < duty ? 1 : -1;
|
|
187
|
+
break;
|
|
188
|
+
case "sawtooth":
|
|
189
|
+
sample = 2 * phase - 1;
|
|
190
|
+
break;
|
|
191
|
+
case "triangle":
|
|
192
|
+
sample = 4 * Math.abs(phase - .5) - 1;
|
|
193
|
+
break;
|
|
194
|
+
case "sine":
|
|
195
|
+
sample = Math.sin(TWO_PI * phase);
|
|
196
|
+
break;
|
|
197
|
+
default:
|
|
198
|
+
noiseHold -= freq / sampleRate;
|
|
199
|
+
if (noiseHold <= 0) {
|
|
200
|
+
lastNoise = rng.range(-1, 1);
|
|
201
|
+
noiseHold += 1;
|
|
202
|
+
}
|
|
203
|
+
sample = lastNoise;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
out[i] = clamp$1(sample * env * params.volume);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function clamp$1(v) {
|
|
211
|
+
return v < -1 ? -1 : v > 1 ? 1 : v;
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/core/audio/spatial.ts
|
|
215
|
+
const ROLLOFF_MODELS = [
|
|
216
|
+
"inverse",
|
|
217
|
+
"linear",
|
|
218
|
+
"exponential"
|
|
219
|
+
];
|
|
220
|
+
/**
|
|
221
|
+
* Gain (0..1) for a source `distance` units from the listener, per the chosen
|
|
222
|
+
* model. Identical to the WebAudio PannerNode formulas so the headless number
|
|
223
|
+
* matches the browser panner:
|
|
224
|
+
*
|
|
225
|
+
* - inverse: ref / (ref + factor·(clamp(d) − ref))
|
|
226
|
+
* - linear: 1 − factor·(clamp(d) − ref) / (max − ref)
|
|
227
|
+
* - exponential: (clamp(d) / ref)^(−factor)
|
|
228
|
+
*
|
|
229
|
+
* where `clamp(d)` is `d` clamped to `[ref, max]`. Always full inside
|
|
230
|
+
* `refDistance`; monotonic non-increasing out to `maxDistance`. Non-finite or
|
|
231
|
+
* degenerate inputs return a safe finite gain.
|
|
232
|
+
*/
|
|
233
|
+
function spatialGain(distance, params) {
|
|
234
|
+
const ref = Math.max(0, params.refDistance);
|
|
235
|
+
const max = Math.max(ref, params.maxDistance);
|
|
236
|
+
const factor = params.rolloffFactor ?? 1;
|
|
237
|
+
if (!Number.isFinite(distance)) return 1;
|
|
238
|
+
const d = clamp(distance, ref, max);
|
|
239
|
+
if (d <= ref) return 1;
|
|
240
|
+
let g;
|
|
241
|
+
switch (params.rolloff) {
|
|
242
|
+
case "linear": {
|
|
243
|
+
const span = max - ref;
|
|
244
|
+
g = span <= 0 ? 1 : 1 - factor * (d - ref) / span;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case "exponential":
|
|
248
|
+
g = ref <= 0 ? 1 : (d / ref) ** -factor;
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
g = ref / (ref + factor * (d - ref));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
if (!Number.isFinite(g)) return 0;
|
|
255
|
+
return clamp01(g);
|
|
256
|
+
}
|
|
257
|
+
const scratchToSource = [
|
|
258
|
+
0,
|
|
259
|
+
0,
|
|
260
|
+
0
|
|
261
|
+
];
|
|
262
|
+
/**
|
|
263
|
+
* Stereo pan (−1 left … 0 center … +1 right) for a source at `sourcePos`,
|
|
264
|
+
* relative to the listener's orientation. Projects the listener→source vector
|
|
265
|
+
* onto the listener's RIGHT axis (forward × up) and normalizes by its length —
|
|
266
|
+
* sign tells left/right, magnitude tells how far off-axis. A source at the
|
|
267
|
+
* listener position (or dead ahead) is centered. Result clamped to [−1, 1].
|
|
268
|
+
*
|
|
269
|
+
* This is the CPU twin of what HRTF/equalpower panning does to the azimuth; the
|
|
270
|
+
* real PannerNode does the full spatialization, but exposing the sign here makes
|
|
271
|
+
* the geometry unit-testable and lets the 2D fallback pan by the same rule.
|
|
272
|
+
*/
|
|
273
|
+
function spatialPan(sourcePos, listener) {
|
|
274
|
+
const dx = sourcePos[0] - listener.position[0];
|
|
275
|
+
const dy = sourcePos[1] - listener.position[1];
|
|
276
|
+
const dz = sourcePos[2] - listener.position[2];
|
|
277
|
+
const len = Math.hypot(dx, dy, dz);
|
|
278
|
+
if (len < 1e-6) return 0;
|
|
279
|
+
scratchToSource[0] = dx / len;
|
|
280
|
+
scratchToSource[1] = dy / len;
|
|
281
|
+
scratchToSource[2] = dz / len;
|
|
282
|
+
const [fx, fy, fz] = listener.forward;
|
|
283
|
+
const [ux, uy, uz] = listener.up;
|
|
284
|
+
const rx = fy * uz - fz * uy;
|
|
285
|
+
const ry = fz * ux - fx * uz;
|
|
286
|
+
const rz = fx * uy - fy * ux;
|
|
287
|
+
const rLen = Math.hypot(rx, ry, rz);
|
|
288
|
+
if (rLen < 1e-6) return 0;
|
|
289
|
+
return clamp((scratchToSource[0] * rx + scratchToSource[1] * ry + scratchToSource[2] * rz) / rLen, -1, 1);
|
|
290
|
+
}
|
|
291
|
+
function clamp(v, lo, hi) {
|
|
292
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
293
|
+
}
|
|
294
|
+
function clamp01(v) {
|
|
295
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
296
|
+
}
|
|
297
|
+
function createElement() {
|
|
298
|
+
if (typeof Audio === "undefined") return null;
|
|
299
|
+
return new Audio();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* One sound. `play()` (or `autoplay: true`) starts it; `finished` fires when
|
|
303
|
+
* it ends. Browsers block audio before the first user gesture — a blocked
|
|
304
|
+
* play marks `pendingGesture`; `createGame` retries pending players on the
|
|
305
|
+
* first pointer/key gesture automatically.
|
|
306
|
+
*
|
|
307
|
+
* Two playback modes:
|
|
308
|
+
* - `preset: 'custom'` (default) → plays the audio file at `src` via an HTMLAudio
|
|
309
|
+
* element (good for music / long clips; supports `loop`).
|
|
310
|
+
* - `preset: 'coin' | 'jump' | …` (a procedural SFX preset) → synthesizes a
|
|
311
|
+
* zero-asset sound through WebAudio: instant, deterministic, overlap-friendly
|
|
312
|
+
* for rapid-fire SFX. `pitch`/`seed` vary it. The art-free audio analog of the
|
|
313
|
+
* particle presets. See `incanto-audio.md` for the full preset list.
|
|
314
|
+
*
|
|
315
|
+
* Volume routes through the engine's buses: `engine.audio.master × bus(sfx|music)
|
|
316
|
+
* × volume`. Set `engine.audio.master`/`sfx`/`music`/`muted` for global control.
|
|
317
|
+
*/
|
|
318
|
+
var AudioPlayer = class extends Node {
|
|
319
|
+
static typeName = "AudioPlayer";
|
|
320
|
+
static signals = ["finished"];
|
|
321
|
+
static props = {
|
|
322
|
+
src: { default: "" },
|
|
323
|
+
preset: {
|
|
324
|
+
default: "custom",
|
|
325
|
+
options: ["custom", ...SFX_PRESET_NAMES]
|
|
326
|
+
},
|
|
327
|
+
volume: { default: 1 },
|
|
328
|
+
pitch: { default: 1 },
|
|
329
|
+
seed: { default: 0 },
|
|
330
|
+
bus: {
|
|
331
|
+
default: "sfx",
|
|
332
|
+
options: ["sfx", "music"]
|
|
333
|
+
},
|
|
334
|
+
loop: { default: false },
|
|
335
|
+
autoplay: { default: false },
|
|
336
|
+
spatial: { default: false },
|
|
337
|
+
refDistance: { default: 1 },
|
|
338
|
+
maxDistance: { default: 50 },
|
|
339
|
+
rolloff: {
|
|
340
|
+
default: "inverse",
|
|
341
|
+
options: [...ROLLOFF_MODELS]
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
/** Audio file url (same resolution rules as scene asset urls). Used when
|
|
345
|
+
* `preset === 'custom'`; ignored for procedural presets. */
|
|
346
|
+
src = "";
|
|
347
|
+
/** A procedural SFX preset name (zero-asset), or 'custom' to use `src`. */
|
|
348
|
+
preset = "custom";
|
|
349
|
+
volume = 1;
|
|
350
|
+
/** Pitch multiplier for procedural presets (1 = unchanged). */
|
|
351
|
+
pitch = 1;
|
|
352
|
+
/** Variation seed for noisy presets (e.g. explosion/hit/step). */
|
|
353
|
+
seed = 0;
|
|
354
|
+
/** Which volume bus this routes through. */
|
|
355
|
+
bus = "sfx";
|
|
356
|
+
loop = false;
|
|
357
|
+
/** Start on the first frame in the tree (subject to the gesture policy). */
|
|
358
|
+
autoplay = false;
|
|
359
|
+
/**
|
|
360
|
+
* 3D positional audio: in a 3D scene the sound pans + attenuates by the
|
|
361
|
+
* emitter's world position relative to the active Camera3D (the listener).
|
|
362
|
+
* Default false → identical non-spatial behavior (back-compat). In a 2D scene
|
|
363
|
+
* spatial is currently ignored (the adapter feeds no pose) — see incanto-audio.
|
|
364
|
+
*/
|
|
365
|
+
spatial = false;
|
|
366
|
+
/** Distance at which spatial gain is full; closer never gets louder. */
|
|
367
|
+
refDistance = 1;
|
|
368
|
+
/** Distance past which spatial gain stops falling. */
|
|
369
|
+
maxDistance = 50;
|
|
370
|
+
/** Spatial attenuation curve: 'inverse' | 'linear' | 'exponential'. */
|
|
371
|
+
rolloff = "inverse";
|
|
372
|
+
/** Loader hook: unknown presets / buses fail at LOAD (agents self-correct). */
|
|
373
|
+
static validateJson(node) {
|
|
374
|
+
const a = node;
|
|
375
|
+
if (a.preset !== "custom" && !SFX_PRESET_NAMES.includes(a.preset)) throw new IncantoError("BAD_FORMAT", `AudioPlayer '${node.name}' preset must be 'custom' or one of [${SFX_PRESET_NAMES.join(", ")}], got '${a.preset}'.`, {
|
|
376
|
+
prop: "preset",
|
|
377
|
+
validOptions: SFX_PRESET_NAMES
|
|
378
|
+
});
|
|
379
|
+
if (a.bus !== "sfx" && a.bus !== "music") throw new IncantoError("BAD_FORMAT", `AudioPlayer '${node.name}' bus must be 'sfx' or 'music', got '${a.bus}'.`, {
|
|
380
|
+
prop: "bus",
|
|
381
|
+
validOptions: ["sfx", "music"]
|
|
382
|
+
});
|
|
383
|
+
if (!ROLLOFF_MODELS.includes(a.rolloff)) throw new IncantoError("BAD_FORMAT", `AudioPlayer '${node.name}' rolloff must be one of [${ROLLOFF_MODELS.join(", ")}], got '${a.rolloff}'.`, {
|
|
384
|
+
prop: "rolloff",
|
|
385
|
+
validOptions: [...ROLLOFF_MODELS]
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/** A play() was blocked by the browser's autoplay policy. */
|
|
389
|
+
pendingGesture = false;
|
|
390
|
+
element = null;
|
|
391
|
+
_playing = false;
|
|
392
|
+
autoplayed = false;
|
|
393
|
+
/** Last spatial pose pushed by the 3D adapter (null until/unless spatial). */
|
|
394
|
+
_spatialPose = null;
|
|
395
|
+
get playing() {
|
|
396
|
+
return this._playing;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* @internal Per-frame spatial feed from the 3D adapter (detected structurally
|
|
400
|
+
* by syncTree). Stores the emitter world position + listener pose; ignored
|
|
401
|
+
* unless `spatial` is on. Headless / 2D scenes never call this.
|
|
402
|
+
*/
|
|
403
|
+
_setSpatialPose(pose) {
|
|
404
|
+
this._spatialPose = pose;
|
|
405
|
+
}
|
|
406
|
+
/** Final gain = engine buses × this volume (1 when not in a tree). */
|
|
407
|
+
gain() {
|
|
408
|
+
const buses = this.tree?.engine?.audio;
|
|
409
|
+
return buses ? buses.effectiveVolume(this.bus, this.volume) : this.volume;
|
|
410
|
+
}
|
|
411
|
+
/** Distance gain for the src/element path (1 when not spatial / no pose). */
|
|
412
|
+
spatialElementGain() {
|
|
413
|
+
if (!this.spatial || !this._spatialPose) return 1;
|
|
414
|
+
const { position, listener } = this._spatialPose;
|
|
415
|
+
const dx = position[0] - listener.position[0];
|
|
416
|
+
const dy = position[1] - listener.position[1];
|
|
417
|
+
const dz = position[2] - listener.position[2];
|
|
418
|
+
return spatialGain(Math.hypot(dx, dy, dz), {
|
|
419
|
+
refDistance: this.refDistance,
|
|
420
|
+
maxDistance: this.maxDistance,
|
|
421
|
+
rolloff: this.rolloff
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/** The `spatial` option for the SFX path, or undefined when not spatial. */
|
|
425
|
+
spatialPlay() {
|
|
426
|
+
if (!this.spatial || !this._spatialPose) return void 0;
|
|
427
|
+
return {
|
|
428
|
+
position: this._spatialPose.position,
|
|
429
|
+
listener: this._spatialPose.listener,
|
|
430
|
+
refDistance: this.refDistance,
|
|
431
|
+
maxDistance: this.maxDistance,
|
|
432
|
+
rolloff: this.rolloff
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
play() {
|
|
436
|
+
if (this.preset !== "custom") {
|
|
437
|
+
this.playPreset();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!this.src) return;
|
|
441
|
+
if (!this.element) {
|
|
442
|
+
this.element = createElement();
|
|
443
|
+
if (!this.element) return;
|
|
444
|
+
this.element.addEventListener("ended", () => {
|
|
445
|
+
if (this.loop) return;
|
|
446
|
+
this._playing = false;
|
|
447
|
+
this.emit("finished");
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const el = this.element;
|
|
451
|
+
el.src = this.src;
|
|
452
|
+
el.volume = this.gain() * this.spatialElementGain();
|
|
453
|
+
el.loop = this.loop;
|
|
454
|
+
this._playing = true;
|
|
455
|
+
this.pendingGesture = false;
|
|
456
|
+
el.play().catch(() => {
|
|
457
|
+
this._playing = false;
|
|
458
|
+
this.pendingGesture = true;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Procedural-SFX path: synthesize the preset and fire it through WebAudio
|
|
463
|
+
* (low-latency, overlap-friendly). Fire-and-forget — no `finished`/`playing`
|
|
464
|
+
* tracking (these are short one-shots, not the element-driven `src` clip).
|
|
465
|
+
* Headless (no AudioContext) → silent no-op. WebAudio also needs a gesture;
|
|
466
|
+
* a suspended context is resumed by the same unlock listener as `src` players.
|
|
467
|
+
*/
|
|
468
|
+
playPreset() {
|
|
469
|
+
const params = SFX_PRESETS[this.preset];
|
|
470
|
+
if (!params) return;
|
|
471
|
+
const sfx = this.tree?.engine?.sfx;
|
|
472
|
+
if (!sfx) return;
|
|
473
|
+
sfx.play(params, this.gain(), {
|
|
474
|
+
pitch: this.pitch,
|
|
475
|
+
seed: this.seed,
|
|
476
|
+
spatial: this.spatialPlay()
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
stop() {
|
|
480
|
+
if (this.element) {
|
|
481
|
+
this.element.pause();
|
|
482
|
+
this.element.currentTime = 0;
|
|
483
|
+
}
|
|
484
|
+
this._playing = false;
|
|
485
|
+
this.pendingGesture = false;
|
|
486
|
+
}
|
|
487
|
+
/** Replay a gesture-blocked play (wired to the first user gesture). */
|
|
488
|
+
retryPending() {
|
|
489
|
+
if (this.pendingGesture) this.play();
|
|
490
|
+
}
|
|
491
|
+
update(_dt) {
|
|
492
|
+
if (this.autoplay && !this.autoplayed) {
|
|
493
|
+
this.autoplayed = true;
|
|
494
|
+
this.play();
|
|
495
|
+
}
|
|
496
|
+
if (this.element && this._playing) {
|
|
497
|
+
this.element.volume = this.gain() * this.spatialElementGain();
|
|
498
|
+
this.element.loop = this.loop;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/core/nodes/timer.ts
|
|
504
|
+
/**
|
|
505
|
+
* The canonical serializable game clock — never `setTimeout` in game logic.
|
|
506
|
+
* Emits `timeout` every `waitTime` seconds (once with `oneShot`).
|
|
507
|
+
*/
|
|
508
|
+
var Timer = class extends Node {
|
|
509
|
+
static typeName = "Timer";
|
|
510
|
+
static signals = ["timeout"];
|
|
511
|
+
static props = {
|
|
512
|
+
waitTime: { default: 1 },
|
|
513
|
+
oneShot: { default: false },
|
|
514
|
+
autostart: { default: false }
|
|
515
|
+
};
|
|
516
|
+
/** Seconds between timeouts. */
|
|
517
|
+
waitTime = 1;
|
|
518
|
+
oneShot = false;
|
|
519
|
+
autostart = false;
|
|
520
|
+
running = false;
|
|
521
|
+
remaining = 0;
|
|
522
|
+
start(time) {
|
|
523
|
+
if (time !== void 0) this.waitTime = time;
|
|
524
|
+
this.remaining = this.waitTime;
|
|
525
|
+
this.running = true;
|
|
526
|
+
}
|
|
527
|
+
stop() {
|
|
528
|
+
this.running = false;
|
|
529
|
+
}
|
|
530
|
+
onReady() {
|
|
531
|
+
if (this.autostart) this.start();
|
|
532
|
+
}
|
|
533
|
+
update(dt) {
|
|
534
|
+
if (!this.running) return;
|
|
535
|
+
if (this.waitTime <= 0) {
|
|
536
|
+
this.stop();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.remaining -= dt;
|
|
540
|
+
while (this.remaining <= 0 && this.running) {
|
|
541
|
+
this.emit("timeout");
|
|
542
|
+
if (this.oneShot) this.stop();
|
|
543
|
+
else if (this.waitTime <= 0) this.stop();
|
|
544
|
+
else this.remaining += this.waitTime;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/core/register.ts
|
|
550
|
+
/**
|
|
551
|
+
* Register the core node types. Call once in your game entry before loading
|
|
552
|
+
* scenes. Registration is explicit — never an import-time side effect — so
|
|
553
|
+
* bundler tree-shaking can never silently drop node types.
|
|
554
|
+
*/
|
|
555
|
+
function registerCoreNodes() {
|
|
556
|
+
registerNode(Node);
|
|
557
|
+
registerNode(Timer);
|
|
558
|
+
registerNode(AudioPlayer);
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
export { spatialGain as a, SFX_PRESET_NAMES as c, ROLLOFF_MODELS as i, synthSfx as l, Timer as n, spatialPan as o, AudioPlayer as r, SFX_PRESETS as s, registerCoreNodes as t };
|