incanto 0.52.0 → 0.54.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/bin/incanto-check.mjs +56 -1
- package/dist/2d.d.ts +95 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +111 -6
- package/dist/3d.js +10 -6
- package/dist/{behavior-uPEuZrUB.d.ts → behavior-CyQoSu4n.d.ts} +212 -10
- package/dist/{create-game-B_e9hJf7.js → create-game-BRt6XKmP.js} +31 -12
- package/dist/{create-game-CGnoypjL.js → create-game-Czzp6ZuE.js} +26 -14
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/{duplicate-B-OtSRFL.js → duplicate-MNLMAcbz.js} +1 -1
- package/dist/editor.js +14 -0
- package/dist/{environment-presets-DSZwsKPs.js → environment-presets-BQ_QsIBY.js} +222 -11
- package/dist/{gameplay-B6jqvYeM.js → gameplay-CZ2yq37J.js} +32 -23
- package/dist/gameplay.d.ts +8 -3
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +178 -6
- package/dist/index.js +9 -9
- package/dist/{loader-B-Gft32x.js → loader-BTkHYrQn.js} +54 -18
- package/dist/{loader-B9iTqs27.d.ts → loader-DhI1jFW_.d.ts} +1 -1
- package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{particle-sim-BzJ1yxoE.d.ts → particle-sim-C5OfBbmU.d.ts} +11 -0
- package/dist/{pathfinding-CAR9DjQQ.d.ts → pathfinding-CXGCpRQe.d.ts} +24 -1
- package/dist/{physics-2d-Dns-oZlE.js → physics-2d-CfWAggJ1.js} +2 -2
- package/dist/{physics-3d-BhI0ehpe.js → physics-3d-Bes-GIuR.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Trx7WHnD.js → register-en63AEZO.js} +163 -4
- package/dist/{register-DVwlnZAZ.js → register-p48lHE2o.js} +433 -36
- package/dist/{replay-BU1CCM15.d.ts → replay-ePMz26jw.d.ts} +1 -1
- package/dist/{replay-BicPOMX0.js → replay-t1pP0gQg.js} +2 -2
- package/dist/{split-screen-paxkQs_q.d.ts → split-screen-BsdOHbzP.d.ts} +1 -1
- package/dist/{split-screen-CZ9ccBBQ.js → split-screen-CSb_uZ6W.js} +2 -2
- package/dist/{sprite-animation-D_p28jwU.js → sprite-animation-7qvUxF6Z.js} +19 -0
- package/dist/{src-5gbZO47I.js → src-vNPQeQ1W.js} +1 -1
- package/dist/{teardown-ks3d5W9n.js → teardown-C7uVSJvx.js} +30 -1
- package/dist/{test-DAojuFdb.js → test-Tnf1nQl3.js} +25 -13
- package/dist/test.d.ts +4 -4
- package/dist/test.js +2 -2
- package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-CCvckvbw.js → agent8-BWaW-D85.js} +1 -1
- package/editor/assets/{debug-CLNCOnbc.js → debug-DLF8rUtr.js} +2 -2
- package/editor/assets/{index-Cb5Brupb.js → index-BYfiwbQx.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +221 -0
- package/skills/incanto-assets.md +45 -2
- package/skills/incanto-audio.md +97 -10
- package/skills/incanto-building-2d-games.md +55 -3
- package/skills/incanto-building-3d-games.md +3 -1
- package/skills/incanto-environment.md +3 -1
- package/skills/incanto-gameplay-behaviors.md +51 -4
- package/skills/incanto-hud.md +7 -0
- package/skills/incanto-node-reference.md +36 -0
- package/skills/incanto-performance.md +47 -0
- package/skills/incanto-physics-and-input.md +38 -0
- package/skills/incanto-verifying-your-game.md +50 -1
- package/skills/incanto-web-integration.md +1 -0
- package/templates-app/beacon-isle-3d/index.html +3 -3
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/index.html +3 -3
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/index.html +3 -3
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -1,25 +1,42 @@
|
|
|
1
|
-
import { C as diagnose, M as Signal, S as InputMap, f as Node, g as restoreBehaviors, h as captureBehaviors, v as createSaveStore, x as resolveOrderGroups, y as ORDER_GROUP_BASE } from "./loader-
|
|
1
|
+
import { C as diagnose, M as Signal, S as InputMap, f as Node, g as restoreBehaviors, h as captureBehaviors, v as createSaveStore, x as resolveOrderGroups, y as ORDER_GROUP_BASE } from "./loader-BTkHYrQn.js";
|
|
2
2
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
3
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
4
4
|
import { l as registerNode } from "./registry-C7u42TID.js";
|
|
5
5
|
//#region src/core/audio/buses.ts
|
|
6
|
-
|
|
7
|
-
* Global volume state — `master` × `sfx`/`music` gain (0..1) plus a `muted`
|
|
8
|
-
* flag. PURE state (no WebAudio): games and AI set it to control overall
|
|
9
|
-
* loudness/mute; the renderer reads `effectiveVolume()` when it actually plays
|
|
10
|
-
* a sound. Lives on the engine as `engine.audio` (instance-scoped, no
|
|
11
|
-
* singletons). The WebAudio wiring is in the adapter; this just holds the
|
|
12
|
-
* numbers so they're fully testable in `node`.
|
|
13
|
-
*
|
|
14
|
-
* ```ts
|
|
15
|
-
* engine.audio.master = 0.8; // dim everything
|
|
16
|
-
* engine.audio.music = 0.4; // quieter background music
|
|
17
|
-
* engine.audio.muted = true; // mute toggle
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
6
|
+
const AUDIO_LOG_LIMIT = 200;
|
|
20
7
|
var AudioBuses = class {
|
|
21
8
|
/** Fires whenever any value changes (drives live re-gain of playing sounds). */
|
|
22
9
|
changed = new Signal();
|
|
10
|
+
/**
|
|
11
|
+
* What has sounded, most recent last.
|
|
12
|
+
*
|
|
13
|
+
* Audio is a no-op headless, which left "did the coin sound actually fire
|
|
14
|
+
* when the coin was collected?" unanswerable — the audio skill's own advice
|
|
15
|
+
* was to *"assert `play()` doesn't throw and check your gameplay logic
|
|
16
|
+
* instead"*, which checks something else. The CALL still happens with no
|
|
17
|
+
* backend, so recording it answers the wiring question everywhere: in the
|
|
18
|
+
* verify VM, in a playtest, in a test.
|
|
19
|
+
*
|
|
20
|
+
* Bounded (200) — a game running for an hour must not grow a log.
|
|
21
|
+
*/
|
|
22
|
+
events = [];
|
|
23
|
+
/** @internal Called by the players; games read `recent()`. */
|
|
24
|
+
record(event) {
|
|
25
|
+
this.events.push(event);
|
|
26
|
+
if (this.events.length > AUDIO_LOG_LIMIT) this.events.shift();
|
|
27
|
+
}
|
|
28
|
+
/** The last `limit` sounds, oldest first. */
|
|
29
|
+
recent(limit = AUDIO_LOG_LIMIT) {
|
|
30
|
+
return this.events.slice(Math.max(0, this.events.length - limit));
|
|
31
|
+
}
|
|
32
|
+
/** How many times a preset/clip/track sounded — the assertion you want. */
|
|
33
|
+
countOf(name) {
|
|
34
|
+
return this.events.reduce((n, e) => e.name === name ? n + 1 : n, 0);
|
|
35
|
+
}
|
|
36
|
+
/** Forget what has sounded so far (between scenes, or between assertions). */
|
|
37
|
+
clearLog() {
|
|
38
|
+
this.events.length = 0;
|
|
39
|
+
}
|
|
23
40
|
_master = 1;
|
|
24
41
|
_sfx = 1;
|
|
25
42
|
_music = 1;
|
|
@@ -191,6 +208,24 @@ var WebAudioMusicBackend = class {
|
|
|
191
208
|
const ctx = this.ensure();
|
|
192
209
|
if (ctx && ctx.state === "suspended") ctx.resume();
|
|
193
210
|
}
|
|
211
|
+
dispose() {
|
|
212
|
+
this.ctx?.close?.();
|
|
213
|
+
this.ctx = null;
|
|
214
|
+
this.resolved = false;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/core/audio/music-manager.ts
|
|
219
|
+
/** Mints silent tracks so the state machine runs with no audio backend. */
|
|
220
|
+
const SILENT_BACKEND = {
|
|
221
|
+
createTrack: (src) => ({
|
|
222
|
+
src,
|
|
223
|
+
setGain() {},
|
|
224
|
+
setLoop() {},
|
|
225
|
+
play() {},
|
|
226
|
+
stop() {}
|
|
227
|
+
}),
|
|
228
|
+
unlock() {}
|
|
194
229
|
};
|
|
195
230
|
/**
|
|
196
231
|
* Single-track background-music manager: plays ONE music track, crossfading to
|
|
@@ -232,13 +267,22 @@ var MusicManager = class {
|
|
|
232
267
|
get volume() {
|
|
233
268
|
return this._volume;
|
|
234
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* The backend, or an INERT one that mints silent tracks.
|
|
272
|
+
*
|
|
273
|
+
* Never null, on purpose: the manager used to return before touching its
|
|
274
|
+
* state when there was no backend, so `current` stayed null forever in every
|
|
275
|
+
* headless run — including the verify VM, which is the only place a game is
|
|
276
|
+
* checked automatically. "Did the boss music start?" had no answer anywhere.
|
|
277
|
+
* With an inert backend the state machine runs identically and nothing plays.
|
|
278
|
+
*/
|
|
235
279
|
ensure() {
|
|
236
280
|
if (!this.resolved) {
|
|
237
281
|
const b = new WebAudioMusicBackend();
|
|
238
282
|
this.backend = b && b.available === false ? null : b;
|
|
239
283
|
this.resolved = true;
|
|
240
284
|
}
|
|
241
|
-
return this.backend;
|
|
285
|
+
return this.backend ?? SILENT_BACKEND;
|
|
242
286
|
}
|
|
243
287
|
/** Resume a gesture-suspended context + (re)start any pending track. */
|
|
244
288
|
unlock() {
|
|
@@ -247,8 +291,14 @@ var MusicManager = class {
|
|
|
247
291
|
}
|
|
248
292
|
/** Play a single track (replacing any current one immediately, no fade). */
|
|
249
293
|
play(src, opts = {}) {
|
|
294
|
+
this.buses.record({
|
|
295
|
+
kind: "music",
|
|
296
|
+
name: src,
|
|
297
|
+
from: "engine.music",
|
|
298
|
+
bus: this._bus,
|
|
299
|
+
at: 0
|
|
300
|
+
});
|
|
250
301
|
const backend = this.ensure();
|
|
251
|
-
if (!backend) return;
|
|
252
302
|
this.killOutgoing();
|
|
253
303
|
this.active?.track.stop();
|
|
254
304
|
this._bus = opts.bus ?? "music";
|
|
@@ -270,8 +320,14 @@ var MusicManager = class {
|
|
|
270
320
|
* it's a fade-in. Crossfading to the already-current src is a no-op.
|
|
271
321
|
*/
|
|
272
322
|
crossfadeTo(src, seconds = 2) {
|
|
323
|
+
this.buses.record({
|
|
324
|
+
kind: "music",
|
|
325
|
+
name: src,
|
|
326
|
+
from: "engine.music",
|
|
327
|
+
bus: this._bus,
|
|
328
|
+
at: 0
|
|
329
|
+
});
|
|
273
330
|
const backend = this.ensure();
|
|
274
|
-
if (!backend) return;
|
|
275
331
|
if (this.active && this.active.track.src === src && this.active.phase !== "fadeOut") return;
|
|
276
332
|
const secs = Math.max(0, seconds);
|
|
277
333
|
if (!this.active) {
|
|
@@ -319,6 +375,18 @@ var MusicManager = class {
|
|
|
319
375
|
};
|
|
320
376
|
this.applyGains();
|
|
321
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Stop instantly and release the backend — music must not outlive the game.
|
|
380
|
+
*
|
|
381
|
+
* `dispose()` is the one call that tears an SPA-mounted game down, and it used
|
|
382
|
+
* to leave the track playing with the engine that owned it gone.
|
|
383
|
+
*/
|
|
384
|
+
dispose() {
|
|
385
|
+
this.stop(0);
|
|
386
|
+
this.backend?.dispose?.();
|
|
387
|
+
this.backend = null;
|
|
388
|
+
this.resolved = false;
|
|
389
|
+
}
|
|
322
390
|
/** Set the per-manager volume (re-applied to live tracks on the next tick). */
|
|
323
391
|
setVolume(v) {
|
|
324
392
|
this._volume = clamp01$1(v);
|
|
@@ -853,6 +921,19 @@ var SfxEngine = class {
|
|
|
853
921
|
ctx = null;
|
|
854
922
|
resolved = false;
|
|
855
923
|
pcmCache = /* @__PURE__ */ new Map();
|
|
924
|
+
/**
|
|
925
|
+
* The engine's buses, when this SfxEngine belongs to one — so a continuous
|
|
926
|
+
* voice lands in the same audio record as everything else. A record that
|
|
927
|
+
* covered only SOME sounds would answer "did the engine hum start?" with a
|
|
928
|
+
* confident no.
|
|
929
|
+
*/
|
|
930
|
+
buses = null;
|
|
931
|
+
/** Live continuous voices, so teardown can silence the ones nobody stopped. */
|
|
932
|
+
voices = /* @__PURE__ */ new Set();
|
|
933
|
+
/** @internal Engine wiring. */
|
|
934
|
+
_setBuses(buses) {
|
|
935
|
+
this.buses = buses;
|
|
936
|
+
}
|
|
856
937
|
/** True once a backend is present (lazily created on first use). */
|
|
857
938
|
get available() {
|
|
858
939
|
return this.ensure() !== null;
|
|
@@ -870,10 +951,45 @@ var SfxEngine = class {
|
|
|
870
951
|
* done. Headless returns an inert handle (same call sites, no ifs).
|
|
871
952
|
*/
|
|
872
953
|
startVoice(preset, gain = 1) {
|
|
954
|
+
this.buses?.record({
|
|
955
|
+
kind: "voice",
|
|
956
|
+
name: preset,
|
|
957
|
+
from: "engine.sfx",
|
|
958
|
+
bus: "sfx",
|
|
959
|
+
at: 0
|
|
960
|
+
});
|
|
873
961
|
const ctx = this.ensure();
|
|
874
962
|
if (!ctx) return INERT_VOICE;
|
|
875
963
|
if (ctx.state === "suspended") ctx.resume();
|
|
876
|
-
|
|
964
|
+
const voice = createVoice(ctx, preset, gain);
|
|
965
|
+
for (const v of this.voices) if (v.stopped) this.voices.delete(v);
|
|
966
|
+
this.voices.add(voice);
|
|
967
|
+
return voice;
|
|
968
|
+
}
|
|
969
|
+
/** How many continuous voices are still running (teardown/leak checks). */
|
|
970
|
+
get liveVoiceCount() {
|
|
971
|
+
let n = 0;
|
|
972
|
+
for (const v of this.voices) if (!v.stopped) n++;
|
|
973
|
+
return n;
|
|
974
|
+
}
|
|
975
|
+
/** Silence every continuous voice — the hum must not outlive the game. */
|
|
976
|
+
stopAll(seconds = 0) {
|
|
977
|
+
for (const v of [...this.voices]) v.stop(seconds);
|
|
978
|
+
this.voices.clear();
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Silence everything and release the AudioContext.
|
|
982
|
+
*
|
|
983
|
+
* A browser allows only a handful of contexts per page, so an SPA that mounts
|
|
984
|
+
* the game a few times must hand this one back — `dispose()` is documented as
|
|
985
|
+
* the call that tears everything down.
|
|
986
|
+
*/
|
|
987
|
+
dispose() {
|
|
988
|
+
this.stopAll();
|
|
989
|
+
this.ctx?.close?.();
|
|
990
|
+
this.ctx = null;
|
|
991
|
+
this.resolved = false;
|
|
992
|
+
this.pcmCache.clear();
|
|
877
993
|
}
|
|
878
994
|
/** Resume a gesture-suspended context (wired to the first user gesture). */
|
|
879
995
|
unlock() {
|
|
@@ -1040,6 +1156,33 @@ function suggestLocale(available, languages) {
|
|
|
1040
1156
|
return "en";
|
|
1041
1157
|
}
|
|
1042
1158
|
//#endregion
|
|
1159
|
+
//#region src/core/effects-log.ts
|
|
1160
|
+
const EFFECT_LOG_LIMIT = 200;
|
|
1161
|
+
var EffectLog = class {
|
|
1162
|
+
events = [];
|
|
1163
|
+
/** @internal Called by the effects themselves; games read `recent()`. */
|
|
1164
|
+
record(event) {
|
|
1165
|
+
this.events.push(event);
|
|
1166
|
+
if (this.events.length > EFFECT_LOG_LIMIT) this.events.shift();
|
|
1167
|
+
}
|
|
1168
|
+
/** The last `limit` effects, oldest first. */
|
|
1169
|
+
recent(limit = EFFECT_LOG_LIMIT) {
|
|
1170
|
+
return this.events.slice(Math.max(0, this.events.length - limit));
|
|
1171
|
+
}
|
|
1172
|
+
/** How many times an effect with this name fired (`explosion`, `#ff0000`). */
|
|
1173
|
+
countOf(name) {
|
|
1174
|
+
return this.events.reduce((n, e) => e.name === name ? n + 1 : n, 0);
|
|
1175
|
+
}
|
|
1176
|
+
/** How many effects this node fired — the question a wiring check has. */
|
|
1177
|
+
countFrom(path) {
|
|
1178
|
+
return this.events.reduce((n, e) => e.from === path ? n + 1 : n, 0);
|
|
1179
|
+
}
|
|
1180
|
+
/** Forget what has fired so far (between scenes, or between assertions). */
|
|
1181
|
+
clearLog() {
|
|
1182
|
+
this.events.length = 0;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
//#endregion
|
|
1043
1186
|
//#region src/core/frame-stats.ts
|
|
1044
1187
|
/**
|
|
1045
1188
|
* Rolling window of REAL frame timestamps — the math behind `engine.stats()`
|
|
@@ -1134,6 +1277,57 @@ var LogManager = class {
|
|
|
1134
1277
|
}
|
|
1135
1278
|
};
|
|
1136
1279
|
//#endregion
|
|
1280
|
+
//#region src/core/phase-timing.ts
|
|
1281
|
+
const EMPTY = {
|
|
1282
|
+
fixedMs: 0,
|
|
1283
|
+
updateMs: 0,
|
|
1284
|
+
renderMs: 0,
|
|
1285
|
+
otherMs: 0
|
|
1286
|
+
};
|
|
1287
|
+
var PhaseTimings = class {
|
|
1288
|
+
capacity;
|
|
1289
|
+
fixed = [];
|
|
1290
|
+
update = [];
|
|
1291
|
+
render = [];
|
|
1292
|
+
head = 0;
|
|
1293
|
+
constructor(capacity = 60) {
|
|
1294
|
+
this.capacity = capacity;
|
|
1295
|
+
}
|
|
1296
|
+
/** Bank one frame's engine-side work. */
|
|
1297
|
+
push(fixedMs, updateMs, renderMs) {
|
|
1298
|
+
const i = this.head % this.capacity;
|
|
1299
|
+
this.fixed[i] = fixedMs;
|
|
1300
|
+
this.update[i] = updateMs;
|
|
1301
|
+
this.render[i] = renderMs;
|
|
1302
|
+
this.head += 1;
|
|
1303
|
+
}
|
|
1304
|
+
/** Means over the window; `frameMs` (0 headless) fills in `otherMs`. */
|
|
1305
|
+
read(frameMs) {
|
|
1306
|
+
const n = Math.min(this.head, this.capacity);
|
|
1307
|
+
if (n === 0) return { ...EMPTY };
|
|
1308
|
+
const fixedMs = mean(this.fixed, n);
|
|
1309
|
+
const updateMs = mean(this.update, n);
|
|
1310
|
+
const renderMs = mean(this.render, n);
|
|
1311
|
+
return {
|
|
1312
|
+
fixedMs,
|
|
1313
|
+
updateMs,
|
|
1314
|
+
renderMs,
|
|
1315
|
+
otherMs: Math.max(0, frameMs - fixedMs - updateMs - renderMs)
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
clear() {
|
|
1319
|
+
this.fixed.length = 0;
|
|
1320
|
+
this.update.length = 0;
|
|
1321
|
+
this.render.length = 0;
|
|
1322
|
+
this.head = 0;
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
function mean(values, n) {
|
|
1326
|
+
let total = 0;
|
|
1327
|
+
for (let i = 0; i < n; i++) total += values[i] ?? 0;
|
|
1328
|
+
return total / n;
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1137
1331
|
//#region src/core/settings.ts
|
|
1138
1332
|
const DEFAULTS = {
|
|
1139
1333
|
master: 1,
|
|
@@ -1432,6 +1626,12 @@ var Engine = class {
|
|
|
1432
1626
|
* routes its volume through this; games set it for global volume/mute. */
|
|
1433
1627
|
audio = new AudioBuses();
|
|
1434
1628
|
/**
|
|
1629
|
+
* What SHOWED — the visual half of the audio record. Particle bursts, camera
|
|
1630
|
+
* shake, screen flash and freeze frames land here, so "did the explosion
|
|
1631
|
+
* fire?" is answerable in a run with no renderer.
|
|
1632
|
+
*/
|
|
1633
|
+
effects = new EffectLog();
|
|
1634
|
+
/**
|
|
1435
1635
|
* Persisted player settings — volume, quality, sensitivity.
|
|
1436
1636
|
*
|
|
1437
1637
|
* `createSaveStore` was good and had zero callers, so every game's volume
|
|
@@ -1474,6 +1674,10 @@ var Engine = class {
|
|
|
1474
1674
|
accumulator = 0;
|
|
1475
1675
|
disposeScheduler = null;
|
|
1476
1676
|
frameStats = new FrameStatsRing();
|
|
1677
|
+
/** Where each frame's time went — the breakdown `frameMs` alone cannot give. */
|
|
1678
|
+
phaseTimings = new PhaseTimings();
|
|
1679
|
+
/** This frame's render cost, as reported by whoever renders. */
|
|
1680
|
+
renderMs = 0;
|
|
1477
1681
|
/**
|
|
1478
1682
|
* Game-time multiplier: 1 = realtime, 0.5 = slow motion, 0 = frozen
|
|
1479
1683
|
* (hit-stop / pause). Scales BOTH variable and fixed updates — physics,
|
|
@@ -1508,6 +1712,7 @@ var Engine = class {
|
|
|
1508
1712
|
this.scheduler = opts.scheduler ?? rafScheduler;
|
|
1509
1713
|
this.rng = new Rng(opts.seed ?? Math.random() * 4294967295 >>> 0);
|
|
1510
1714
|
this.maxFps = opts.maxFps ?? 0;
|
|
1715
|
+
this.sfx._setBuses(this.audio);
|
|
1511
1716
|
}
|
|
1512
1717
|
get scene() {
|
|
1513
1718
|
return this._scene;
|
|
@@ -1664,12 +1869,24 @@ var Engine = class {
|
|
|
1664
1869
|
this.lastFrameError = null;
|
|
1665
1870
|
return nodes.length;
|
|
1666
1871
|
}
|
|
1872
|
+
/**
|
|
1873
|
+
* How long the last render took, in ms — called by whoever renders.
|
|
1874
|
+
*
|
|
1875
|
+
* The engine does not render, so this is the one slice it cannot measure for
|
|
1876
|
+
* itself; without it the cost lands in `otherMs` beside the GC and the two
|
|
1877
|
+
* are indistinguishable, which is the confusion this whole breakdown exists
|
|
1878
|
+
* to end.
|
|
1879
|
+
*/
|
|
1880
|
+
noteRenderMs(ms) {
|
|
1881
|
+
this.renderMs = ms;
|
|
1882
|
+
}
|
|
1667
1883
|
stop() {
|
|
1668
1884
|
this.disposeScheduler?.();
|
|
1669
1885
|
this.disposeScheduler = null;
|
|
1670
1886
|
this.lastMs = null;
|
|
1671
1887
|
this.accumulator = 0;
|
|
1672
1888
|
this.frameStats.clear();
|
|
1889
|
+
this.phaseTimings.clear();
|
|
1673
1890
|
}
|
|
1674
1891
|
/**
|
|
1675
1892
|
* Live performance counters, queryable at ANY time. fps/frameMs come from a
|
|
@@ -1683,7 +1900,8 @@ var Engine = class {
|
|
|
1683
1900
|
frameMs: this.frameStats.frameMs(),
|
|
1684
1901
|
nodes: this._scene?.tree.root ? countNodes(this._scene.tree.root) : 0,
|
|
1685
1902
|
running: this.disposeScheduler !== null,
|
|
1686
|
-
errors: this.errorCount
|
|
1903
|
+
errors: this.errorCount,
|
|
1904
|
+
phases: this.phaseTimings.read(this.frameStats.frameMs())
|
|
1687
1905
|
};
|
|
1688
1906
|
}
|
|
1689
1907
|
/**
|
|
@@ -1704,6 +1922,8 @@ var Engine = class {
|
|
|
1704
1922
|
*/
|
|
1705
1923
|
dispose() {
|
|
1706
1924
|
this.stop();
|
|
1925
|
+
this.music.dispose();
|
|
1926
|
+
this.sfx.dispose();
|
|
1707
1927
|
if (this._scene) {
|
|
1708
1928
|
this._scene.tree._setEngine(null);
|
|
1709
1929
|
this._scene.root.free();
|
|
@@ -1715,21 +1935,31 @@ var Engine = class {
|
|
|
1715
1935
|
}
|
|
1716
1936
|
/**
|
|
1717
1937
|
* Advance exactly ONE fixed step and one variable update (both dt = the
|
|
1718
|
-
* fixed step), bypassing the wall-clock accumulator
|
|
1719
|
-
* by construction — the unit of time for headless
|
|
1720
|
-
* runScript): every input edge is visible to BOTH
|
|
1721
|
-
* exactly once.
|
|
1938
|
+
* fixed step scaled by `timeScale`), bypassing the wall-clock accumulator
|
|
1939
|
+
* entirely. Drift-free by construction — the unit of time for headless
|
|
1940
|
+
* harnesses (incanto/test runScript): every input edge is visible to BOTH
|
|
1941
|
+
* fixedUpdate and update exactly once.
|
|
1942
|
+
*
|
|
1943
|
+
* `ignorePause: true` steps a frame at full scale even while the game is
|
|
1944
|
+
* paused — the debug overlay's single-step button, and nothing else.
|
|
1722
1945
|
*/
|
|
1723
|
-
step() {
|
|
1946
|
+
step(opts = {}) {
|
|
1724
1947
|
const scene = this._scene;
|
|
1725
1948
|
if (!scene) return;
|
|
1726
|
-
this.
|
|
1949
|
+
const dt = this.fixedStep * (opts.ignorePause ? 1 : this.timeScale);
|
|
1950
|
+
this._time += dt;
|
|
1727
1951
|
this._unscaledTime += this.fixedStep;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1952
|
+
const startedFixed = performance.now();
|
|
1953
|
+
scene.tree.fixedUpdate(dt);
|
|
1954
|
+
this.fixedUpdated.emit(dt);
|
|
1955
|
+
const startedUpdate = performance.now();
|
|
1956
|
+
scene.tree.update(dt);
|
|
1957
|
+
this.music.tick(dt);
|
|
1958
|
+
this.updated.emit(dt);
|
|
1959
|
+
const ended = performance.now();
|
|
1960
|
+
const updateMs = Math.max(0, ended - startedUpdate - this.renderMs);
|
|
1961
|
+
this.phaseTimings.push(startedUpdate - startedFixed, updateMs, this.renderMs);
|
|
1962
|
+
this.renderMs = 0;
|
|
1733
1963
|
this.input.endFrame();
|
|
1734
1964
|
}
|
|
1735
1965
|
/** Advance the loop manually. First call after (re)start only primes the clock. */
|
|
@@ -1748,6 +1978,7 @@ var Engine = class {
|
|
|
1748
1978
|
if (!scene) return;
|
|
1749
1979
|
this.accumulator += dt;
|
|
1750
1980
|
let steps = 0;
|
|
1981
|
+
const startedFixed = performance.now();
|
|
1751
1982
|
while (this.accumulator >= this.fixedStep && steps < this.maxFixedSteps) {
|
|
1752
1983
|
scene.tree.fixedUpdate(this.fixedStep);
|
|
1753
1984
|
this.fixedUpdated.emit(this.fixedStep);
|
|
@@ -1755,9 +1986,14 @@ var Engine = class {
|
|
|
1755
1986
|
steps += 1;
|
|
1756
1987
|
}
|
|
1757
1988
|
if (this.accumulator >= this.fixedStep) this.accumulator %= this.fixedStep;
|
|
1989
|
+
const startedUpdate = performance.now();
|
|
1758
1990
|
scene.tree.update(dt);
|
|
1759
1991
|
this.music.tick(dt);
|
|
1760
1992
|
this.updated.emit(dt);
|
|
1993
|
+
const ended = performance.now();
|
|
1994
|
+
const updateMs = Math.max(0, ended - startedUpdate - this.renderMs);
|
|
1995
|
+
this.phaseTimings.push(startedUpdate - startedFixed, updateMs, this.renderMs);
|
|
1996
|
+
this.renderMs = 0;
|
|
1761
1997
|
this.input.endFrame();
|
|
1762
1998
|
}
|
|
1763
1999
|
};
|
|
@@ -1859,6 +2095,11 @@ var AudioPlayer = class extends Node {
|
|
|
1859
2095
|
}
|
|
1860
2096
|
/** A play() was blocked by the browser's autoplay policy. */
|
|
1861
2097
|
pendingGesture = false;
|
|
2098
|
+
/**
|
|
2099
|
+
* Why this player is silent, when the reason is the FILE rather than the
|
|
2100
|
+
* autoplay gate. Read by `game.assetErrors()`.
|
|
2101
|
+
*/
|
|
2102
|
+
loadError = null;
|
|
1862
2103
|
element = null;
|
|
1863
2104
|
_playing = false;
|
|
1864
2105
|
autoplayed = false;
|
|
@@ -1875,6 +2116,23 @@ var AudioPlayer = class extends Node {
|
|
|
1875
2116
|
_setSpatialPose(pose) {
|
|
1876
2117
|
this._spatialPose = pose;
|
|
1877
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Write this sound into the engine's audio record.
|
|
2121
|
+
*
|
|
2122
|
+
* The CALL happens even where the backend does not (headless, the verify VM),
|
|
2123
|
+
* which is exactly why the record is worth keeping: it answers "did the coin
|
|
2124
|
+
* sound fire when the coin was collected" in places no sound can be heard.
|
|
2125
|
+
*/
|
|
2126
|
+
note(kind, name) {
|
|
2127
|
+
const engine = this.tree?.engine;
|
|
2128
|
+
engine?.audio.record({
|
|
2129
|
+
kind,
|
|
2130
|
+
name,
|
|
2131
|
+
from: this.getPath(),
|
|
2132
|
+
bus: this.bus === "music" ? "music" : "sfx",
|
|
2133
|
+
at: engine.time
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
1878
2136
|
/** Final gain = engine buses × this volume (1 when not in a tree). */
|
|
1879
2137
|
gain() {
|
|
1880
2138
|
const buses = this.tree?.engine?.audio;
|
|
@@ -1906,10 +2164,12 @@ var AudioPlayer = class extends Node {
|
|
|
1906
2164
|
}
|
|
1907
2165
|
play() {
|
|
1908
2166
|
if (this.preset !== "custom") {
|
|
2167
|
+
this.note("preset", this.preset);
|
|
1909
2168
|
this.playPreset();
|
|
1910
2169
|
return;
|
|
1911
2170
|
}
|
|
1912
2171
|
if (!this.src) return;
|
|
2172
|
+
this.note("src", this.src);
|
|
1913
2173
|
if (!this.element) {
|
|
1914
2174
|
this.element = createElement();
|
|
1915
2175
|
if (!this.element) return;
|
|
@@ -1918,6 +2178,12 @@ var AudioPlayer = class extends Node {
|
|
|
1918
2178
|
this._playing = false;
|
|
1919
2179
|
this.emit("finished");
|
|
1920
2180
|
});
|
|
2181
|
+
this.element.addEventListener("error", () => {
|
|
2182
|
+
this._playing = false;
|
|
2183
|
+
if (this.loadError) return;
|
|
2184
|
+
this.loadError = `could not load ${this.src}`;
|
|
2185
|
+
diagnose(this.tree?.engine ?? null, "error", `[incanto] AudioPlayer '${this.name}' could not load '${this.src}' — it is silent.`);
|
|
2186
|
+
});
|
|
1921
2187
|
}
|
|
1922
2188
|
const el = this.element;
|
|
1923
2189
|
el.src = this.src;
|
|
@@ -1925,9 +2191,15 @@ var AudioPlayer = class extends Node {
|
|
|
1925
2191
|
el.loop = this.loop;
|
|
1926
2192
|
this._playing = true;
|
|
1927
2193
|
this.pendingGesture = false;
|
|
1928
|
-
el.play().catch(() => {
|
|
2194
|
+
el.play().catch((error) => {
|
|
1929
2195
|
this._playing = false;
|
|
1930
|
-
|
|
2196
|
+
const name = error?.name ?? "";
|
|
2197
|
+
if (name !== "NotSupportedError") {
|
|
2198
|
+
this.pendingGesture = true;
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
this.loadError = `${name}: ${this.src}`;
|
|
2202
|
+
diagnose(this.tree?.engine ?? null, "error", `[incanto] AudioPlayer '${this.name}' cannot play '${this.src}' (${name}) — it is silent.`);
|
|
1931
2203
|
});
|
|
1932
2204
|
}
|
|
1933
2205
|
/**
|
|
@@ -1948,6 +2220,19 @@ var AudioPlayer = class extends Node {
|
|
|
1948
2220
|
spatial: this.spatialPlay()
|
|
1949
2221
|
});
|
|
1950
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* A destroyed node goes SILENT.
|
|
2225
|
+
*
|
|
2226
|
+
* Nothing used to stop a sound on teardown, so a level's looping background
|
|
2227
|
+
* clip kept playing over the level after it — the node was freed, so nobody
|
|
2228
|
+
* had a handle to stop it with either. Being MOVED is deliberately not this:
|
|
2229
|
+
* reparenting a node is not destroying it, and a sound that survives the move
|
|
2230
|
+
* is what a scene graph should do.
|
|
2231
|
+
*/
|
|
2232
|
+
free() {
|
|
2233
|
+
this.stop();
|
|
2234
|
+
super.free();
|
|
2235
|
+
}
|
|
1951
2236
|
stop() {
|
|
1952
2237
|
if (this.element) {
|
|
1953
2238
|
this.element.pause();
|
|
@@ -2805,6 +3090,17 @@ function px(n) {
|
|
|
2805
3090
|
return `${n}px`;
|
|
2806
3091
|
}
|
|
2807
3092
|
/**
|
|
3093
|
+
* The translated string, or the English one.
|
|
3094
|
+
*
|
|
3095
|
+
* `locale.t` returns the KEY when nothing declares it, and a menu reading
|
|
3096
|
+
* `settings.volume.music` is worse than one reading `Music`. English is the
|
|
3097
|
+
* base language and the fallback, always.
|
|
3098
|
+
*/
|
|
3099
|
+
function translated(engine, key, fallback) {
|
|
3100
|
+
const locale = engine?.locale;
|
|
3101
|
+
return locale?.has(key) ? locale.t(key) : fallback;
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
2808
3104
|
* A box that holds other widgets — the thing a menu actually is.
|
|
2809
3105
|
*
|
|
2810
3106
|
* Widgets normally mount into their HudLayer's anchor slot, which is why a
|
|
@@ -3227,8 +3523,7 @@ var UiSettingSelect = class extends UiSelect {
|
|
|
3227
3523
|
* base language and the fallback, always.
|
|
3228
3524
|
*/
|
|
3229
3525
|
_tOr(key, fallback) {
|
|
3230
|
-
|
|
3231
|
-
return locale?.has(key) ? locale.t(key) : fallback;
|
|
3526
|
+
return translated(this._engine(), key, fallback);
|
|
3232
3527
|
}
|
|
3233
3528
|
_sync() {
|
|
3234
3529
|
const engine = this._engine();
|
|
@@ -3356,6 +3651,106 @@ var UiRenderScaleSelect = class extends UiSettingSelect {
|
|
|
3356
3651
|
return Number.isFinite(pct) ? `${pct}%` : value;
|
|
3357
3652
|
}
|
|
3358
3653
|
};
|
|
3654
|
+
/** The three buses a player is ever offered a slider for. */
|
|
3655
|
+
const VOLUME_BUSES = [
|
|
3656
|
+
"master",
|
|
3657
|
+
"sfx",
|
|
3658
|
+
"music"
|
|
3659
|
+
];
|
|
3660
|
+
const VOLUME_LABELS = {
|
|
3661
|
+
master: "Volume",
|
|
3662
|
+
sfx: "Sound",
|
|
3663
|
+
music: "Music"
|
|
3664
|
+
};
|
|
3665
|
+
/**
|
|
3666
|
+
* The volume slider — the settings row every game has.
|
|
3667
|
+
*
|
|
3668
|
+
* ```json
|
|
3669
|
+
* { "name": "Music", "type": "UiVolumeSlider", "props": { "bus": "music" } }
|
|
3670
|
+
* ```
|
|
3671
|
+
*
|
|
3672
|
+
* The graphics controls above became nodes because a game whose whole world is
|
|
3673
|
+
* JSON could not put a menu on screen without a behavior, a signal connection
|
|
3674
|
+
* and the knowledge that `Settings` persists. All of that was just as true of
|
|
3675
|
+
* volume, which is the control a player looks for FIRST and the only one some
|
|
3676
|
+
* games need at all — so an audio menu meant hand-rolled DOM, and hand-rolled
|
|
3677
|
+
* DOM is out of scene JSON, out of the editor, and out of every headless check.
|
|
3678
|
+
*
|
|
3679
|
+
* It writes the bus; the bus persists itself (`bindAudio`, which
|
|
3680
|
+
* `createGame2D/3D` wire), so there is nothing to save.
|
|
3681
|
+
*/
|
|
3682
|
+
var UiVolumeSlider = class extends UiSlider {
|
|
3683
|
+
static typeName = "UiVolumeSlider";
|
|
3684
|
+
static props = {
|
|
3685
|
+
...UiSlider.props,
|
|
3686
|
+
/** Which bus this row owns. `master` dims everything. */
|
|
3687
|
+
bus: {
|
|
3688
|
+
default: "master",
|
|
3689
|
+
options: [...VOLUME_BUSES]
|
|
3690
|
+
},
|
|
3691
|
+
/** Empty takes the bus's own name — a row nobody labelled still reads. */
|
|
3692
|
+
label: { default: "" }
|
|
3693
|
+
};
|
|
3694
|
+
bus = "master";
|
|
3695
|
+
/** Last value we PUSHED in, to tell an engine-side change from a player drag. */
|
|
3696
|
+
lastPushed = null;
|
|
3697
|
+
_build() {
|
|
3698
|
+
if (!this.label) {
|
|
3699
|
+
const english = VOLUME_LABELS[this.bus] ?? VOLUME_LABELS.master;
|
|
3700
|
+
this.label = translated(this.tree?.engine ?? null, `settings.volume.${this.bus}`, english);
|
|
3701
|
+
}
|
|
3702
|
+
return super._build();
|
|
3703
|
+
}
|
|
3704
|
+
_sync() {
|
|
3705
|
+
const audio = this.tree?.engine?.audio;
|
|
3706
|
+
if (audio) {
|
|
3707
|
+
const stored = audio[this.bus];
|
|
3708
|
+
if (stored !== this.lastPushed) {
|
|
3709
|
+
this.lastPushed = stored;
|
|
3710
|
+
this.value = stored;
|
|
3711
|
+
} else if (this.value !== stored) {
|
|
3712
|
+
audio[this.bus] = this.value;
|
|
3713
|
+
this.lastPushed = audio[this.bus];
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
super._sync();
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
/**
|
|
3720
|
+
* The mute switch.
|
|
3721
|
+
*
|
|
3722
|
+
* ```json
|
|
3723
|
+
* { "name": "Mute", "type": "UiMuteToggle" }
|
|
3724
|
+
* ```
|
|
3725
|
+
*
|
|
3726
|
+
* Checked means silent. Like the slider it writes `engine.audio`, which
|
|
3727
|
+
* persists itself.
|
|
3728
|
+
*/
|
|
3729
|
+
var UiMuteToggle = class extends UiToggle {
|
|
3730
|
+
static typeName = "UiMuteToggle";
|
|
3731
|
+
static props = {
|
|
3732
|
+
...UiToggle.props,
|
|
3733
|
+
label: { default: "" }
|
|
3734
|
+
};
|
|
3735
|
+
lastPushed = null;
|
|
3736
|
+
_build() {
|
|
3737
|
+
if (!this.label) this.label = translated(this.tree?.engine ?? null, "settings.mute", "Mute");
|
|
3738
|
+
return super._build();
|
|
3739
|
+
}
|
|
3740
|
+
_sync() {
|
|
3741
|
+
const audio = this.tree?.engine?.audio;
|
|
3742
|
+
if (audio) {
|
|
3743
|
+
if (audio.muted !== this.lastPushed) {
|
|
3744
|
+
this.lastPushed = audio.muted;
|
|
3745
|
+
this.value = audio.muted;
|
|
3746
|
+
} else if (this.value !== audio.muted) {
|
|
3747
|
+
audio.muted = this.value;
|
|
3748
|
+
this.lastPushed = audio.muted;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
super._sync();
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3359
3754
|
//#endregion
|
|
3360
3755
|
//#region src/core/register.ts
|
|
3361
3756
|
/**
|
|
@@ -3382,6 +3777,8 @@ function registerCoreNodes() {
|
|
|
3382
3777
|
registerNode(UiQualitySelect);
|
|
3383
3778
|
registerNode(UiFrameCapSelect);
|
|
3384
3779
|
registerNode(UiRenderScaleSelect);
|
|
3780
|
+
registerNode(UiVolumeSlider);
|
|
3781
|
+
registerNode(UiMuteToggle);
|
|
3385
3782
|
}
|
|
3386
3783
|
//#endregion
|
|
3387
|
-
export {
|
|
3784
|
+
export { BASE_LOCALE as A, SFX_PRESETS as B, qualityCaps as C, suggestQuality as D, readDeviceHints as E, SfxEngine as F, crossfadeGains as G, synthSfx as H, isAudioContextAvailable as I, fadeGain as K, ROLLOFF_MODELS as L, T_PREFIX as M, suggestLocale as N, LogManager as O, translationKey as P, spatialGain as R, Settings as S, qualityRendering as T, MusicManager as U, SFX_PRESET_NAMES as V, WebAudioMusicBackend as W, UiBanner as _, UiMuteToggle as a, AudioPlayer as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, HudLayer as g, UiDialogue as h, UiLanguageSelect as i, Localization as j, EffectLog as k, UiSelect as l, UiButton as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, AudioBuses as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, UiBar as v, qualityEnvironment as w, Engine as x, UiText as y, spatialPan as z };
|