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.
Files changed (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. 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 };