incanto 0.52.0 → 0.53.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/dist/2d.d.ts +51 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-uPEuZrUB.d.ts → behavior-Do0Da56m.d.ts} +98 -0
- package/dist/{create-game-B_e9hJf7.js → create-game-9D87XaiX.js} +26 -11
- package/dist/{create-game-CGnoypjL.js → create-game-CDJ1lVqK.js} +21 -13
- package/dist/debug.d.ts +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-SkGanr2s.js} +2 -2
- package/dist/{gameplay-B6jqvYeM.js → gameplay-CRYw_Q-T.js} +2 -2
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +177 -5
- package/dist/index.js +8 -8
- package/dist/{loader-B-Gft32x.js → loader-BTkHYrQn.js} +54 -18
- package/dist/{loader-B9iTqs27.d.ts → loader-Cu_7kJDy.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/{pathfinding-CAR9DjQQ.d.ts → pathfinding-HNGFqGUZ.d.ts} +24 -1
- package/dist/{physics-2d-Dns-oZlE.js → physics-2d-BhZZ-HAp.js} +2 -2
- package/dist/{physics-3d-BhI0ehpe.js → physics-3d-DmyO2oaN.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-DVwlnZAZ.js → register-BTomIiYG.js} +304 -24
- package/dist/{register-Trx7WHnD.js → register-DROK2l7J.js} +2 -2
- package/dist/{replay-BU1CCM15.d.ts → replay-9Fy6C10F.d.ts} +1 -1
- package/dist/{replay-BicPOMX0.js → replay-DYNUL4BU.js} +2 -2
- package/dist/{split-screen-CZ9ccBBQ.js → split-screen-CaF9hO7g.js} +2 -2
- package/dist/{split-screen-paxkQs_q.d.ts → split-screen-DNmcX1Pz.d.ts} +1 -1
- package/dist/{src-5gbZO47I.js → src-C9xyZW7M.js} +1 -1
- package/dist/{teardown-ks3d5W9n.js → teardown-Bw2aeyGI.js} +30 -1
- package/dist/{test-DAojuFdb.js → test-CJRsciFk.js} +11 -11
- 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-PlFHzJsh.js} +1 -1
- package/editor/assets/{debug-CLNCOnbc.js → debug-CebV7CDW.js} +1 -1
- package/editor/assets/{index-Cb5Brupb.js → index-CdbsY31G.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +213 -0
- package/skills/incanto-audio.md +97 -10
- package/skills/incanto-building-2d-games.md +20 -3
- package/skills/incanto-hud.md +7 -0
- package/skills/incanto-node-reference.md +34 -0
- package/skills/incanto-physics-and-input.md +38 -0
- package/skills/incanto-verifying-your-game.md +19 -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() {
|
|
@@ -1508,6 +1624,7 @@ var Engine = class {
|
|
|
1508
1624
|
this.scheduler = opts.scheduler ?? rafScheduler;
|
|
1509
1625
|
this.rng = new Rng(opts.seed ?? Math.random() * 4294967295 >>> 0);
|
|
1510
1626
|
this.maxFps = opts.maxFps ?? 0;
|
|
1627
|
+
this.sfx._setBuses(this.audio);
|
|
1511
1628
|
}
|
|
1512
1629
|
get scene() {
|
|
1513
1630
|
return this._scene;
|
|
@@ -1704,6 +1821,8 @@ var Engine = class {
|
|
|
1704
1821
|
*/
|
|
1705
1822
|
dispose() {
|
|
1706
1823
|
this.stop();
|
|
1824
|
+
this.music.dispose();
|
|
1825
|
+
this.sfx.dispose();
|
|
1707
1826
|
if (this._scene) {
|
|
1708
1827
|
this._scene.tree._setEngine(null);
|
|
1709
1828
|
this._scene.root.free();
|
|
@@ -1859,6 +1978,11 @@ var AudioPlayer = class extends Node {
|
|
|
1859
1978
|
}
|
|
1860
1979
|
/** A play() was blocked by the browser's autoplay policy. */
|
|
1861
1980
|
pendingGesture = false;
|
|
1981
|
+
/**
|
|
1982
|
+
* Why this player is silent, when the reason is the FILE rather than the
|
|
1983
|
+
* autoplay gate. Read by `game.assetErrors()`.
|
|
1984
|
+
*/
|
|
1985
|
+
loadError = null;
|
|
1862
1986
|
element = null;
|
|
1863
1987
|
_playing = false;
|
|
1864
1988
|
autoplayed = false;
|
|
@@ -1875,6 +1999,23 @@ var AudioPlayer = class extends Node {
|
|
|
1875
1999
|
_setSpatialPose(pose) {
|
|
1876
2000
|
this._spatialPose = pose;
|
|
1877
2001
|
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Write this sound into the engine's audio record.
|
|
2004
|
+
*
|
|
2005
|
+
* The CALL happens even where the backend does not (headless, the verify VM),
|
|
2006
|
+
* which is exactly why the record is worth keeping: it answers "did the coin
|
|
2007
|
+
* sound fire when the coin was collected" in places no sound can be heard.
|
|
2008
|
+
*/
|
|
2009
|
+
note(kind, name) {
|
|
2010
|
+
const engine = this.tree?.engine;
|
|
2011
|
+
engine?.audio.record({
|
|
2012
|
+
kind,
|
|
2013
|
+
name,
|
|
2014
|
+
from: this.getPath(),
|
|
2015
|
+
bus: this.bus === "music" ? "music" : "sfx",
|
|
2016
|
+
at: engine.time
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
1878
2019
|
/** Final gain = engine buses × this volume (1 when not in a tree). */
|
|
1879
2020
|
gain() {
|
|
1880
2021
|
const buses = this.tree?.engine?.audio;
|
|
@@ -1906,10 +2047,12 @@ var AudioPlayer = class extends Node {
|
|
|
1906
2047
|
}
|
|
1907
2048
|
play() {
|
|
1908
2049
|
if (this.preset !== "custom") {
|
|
2050
|
+
this.note("preset", this.preset);
|
|
1909
2051
|
this.playPreset();
|
|
1910
2052
|
return;
|
|
1911
2053
|
}
|
|
1912
2054
|
if (!this.src) return;
|
|
2055
|
+
this.note("src", this.src);
|
|
1913
2056
|
if (!this.element) {
|
|
1914
2057
|
this.element = createElement();
|
|
1915
2058
|
if (!this.element) return;
|
|
@@ -1918,6 +2061,12 @@ var AudioPlayer = class extends Node {
|
|
|
1918
2061
|
this._playing = false;
|
|
1919
2062
|
this.emit("finished");
|
|
1920
2063
|
});
|
|
2064
|
+
this.element.addEventListener("error", () => {
|
|
2065
|
+
this._playing = false;
|
|
2066
|
+
if (this.loadError) return;
|
|
2067
|
+
this.loadError = `could not load ${this.src}`;
|
|
2068
|
+
diagnose(this.tree?.engine ?? null, "error", `[incanto] AudioPlayer '${this.name}' could not load '${this.src}' — it is silent.`);
|
|
2069
|
+
});
|
|
1921
2070
|
}
|
|
1922
2071
|
const el = this.element;
|
|
1923
2072
|
el.src = this.src;
|
|
@@ -1925,9 +2074,15 @@ var AudioPlayer = class extends Node {
|
|
|
1925
2074
|
el.loop = this.loop;
|
|
1926
2075
|
this._playing = true;
|
|
1927
2076
|
this.pendingGesture = false;
|
|
1928
|
-
el.play().catch(() => {
|
|
2077
|
+
el.play().catch((error) => {
|
|
1929
2078
|
this._playing = false;
|
|
1930
|
-
|
|
2079
|
+
const name = error?.name ?? "";
|
|
2080
|
+
if (name !== "NotSupportedError") {
|
|
2081
|
+
this.pendingGesture = true;
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
this.loadError = `${name}: ${this.src}`;
|
|
2085
|
+
diagnose(this.tree?.engine ?? null, "error", `[incanto] AudioPlayer '${this.name}' cannot play '${this.src}' (${name}) — it is silent.`);
|
|
1931
2086
|
});
|
|
1932
2087
|
}
|
|
1933
2088
|
/**
|
|
@@ -1948,6 +2103,19 @@ var AudioPlayer = class extends Node {
|
|
|
1948
2103
|
spatial: this.spatialPlay()
|
|
1949
2104
|
});
|
|
1950
2105
|
}
|
|
2106
|
+
/**
|
|
2107
|
+
* A destroyed node goes SILENT.
|
|
2108
|
+
*
|
|
2109
|
+
* Nothing used to stop a sound on teardown, so a level's looping background
|
|
2110
|
+
* clip kept playing over the level after it — the node was freed, so nobody
|
|
2111
|
+
* had a handle to stop it with either. Being MOVED is deliberately not this:
|
|
2112
|
+
* reparenting a node is not destroying it, and a sound that survives the move
|
|
2113
|
+
* is what a scene graph should do.
|
|
2114
|
+
*/
|
|
2115
|
+
free() {
|
|
2116
|
+
this.stop();
|
|
2117
|
+
super.free();
|
|
2118
|
+
}
|
|
1951
2119
|
stop() {
|
|
1952
2120
|
if (this.element) {
|
|
1953
2121
|
this.element.pause();
|
|
@@ -2805,6 +2973,17 @@ function px(n) {
|
|
|
2805
2973
|
return `${n}px`;
|
|
2806
2974
|
}
|
|
2807
2975
|
/**
|
|
2976
|
+
* The translated string, or the English one.
|
|
2977
|
+
*
|
|
2978
|
+
* `locale.t` returns the KEY when nothing declares it, and a menu reading
|
|
2979
|
+
* `settings.volume.music` is worse than one reading `Music`. English is the
|
|
2980
|
+
* base language and the fallback, always.
|
|
2981
|
+
*/
|
|
2982
|
+
function translated(engine, key, fallback) {
|
|
2983
|
+
const locale = engine?.locale;
|
|
2984
|
+
return locale?.has(key) ? locale.t(key) : fallback;
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2808
2987
|
* A box that holds other widgets — the thing a menu actually is.
|
|
2809
2988
|
*
|
|
2810
2989
|
* Widgets normally mount into their HudLayer's anchor slot, which is why a
|
|
@@ -3227,8 +3406,7 @@ var UiSettingSelect = class extends UiSelect {
|
|
|
3227
3406
|
* base language and the fallback, always.
|
|
3228
3407
|
*/
|
|
3229
3408
|
_tOr(key, fallback) {
|
|
3230
|
-
|
|
3231
|
-
return locale?.has(key) ? locale.t(key) : fallback;
|
|
3409
|
+
return translated(this._engine(), key, fallback);
|
|
3232
3410
|
}
|
|
3233
3411
|
_sync() {
|
|
3234
3412
|
const engine = this._engine();
|
|
@@ -3356,6 +3534,106 @@ var UiRenderScaleSelect = class extends UiSettingSelect {
|
|
|
3356
3534
|
return Number.isFinite(pct) ? `${pct}%` : value;
|
|
3357
3535
|
}
|
|
3358
3536
|
};
|
|
3537
|
+
/** The three buses a player is ever offered a slider for. */
|
|
3538
|
+
const VOLUME_BUSES = [
|
|
3539
|
+
"master",
|
|
3540
|
+
"sfx",
|
|
3541
|
+
"music"
|
|
3542
|
+
];
|
|
3543
|
+
const VOLUME_LABELS = {
|
|
3544
|
+
master: "Volume",
|
|
3545
|
+
sfx: "Sound",
|
|
3546
|
+
music: "Music"
|
|
3547
|
+
};
|
|
3548
|
+
/**
|
|
3549
|
+
* The volume slider — the settings row every game has.
|
|
3550
|
+
*
|
|
3551
|
+
* ```json
|
|
3552
|
+
* { "name": "Music", "type": "UiVolumeSlider", "props": { "bus": "music" } }
|
|
3553
|
+
* ```
|
|
3554
|
+
*
|
|
3555
|
+
* The graphics controls above became nodes because a game whose whole world is
|
|
3556
|
+
* JSON could not put a menu on screen without a behavior, a signal connection
|
|
3557
|
+
* and the knowledge that `Settings` persists. All of that was just as true of
|
|
3558
|
+
* volume, which is the control a player looks for FIRST and the only one some
|
|
3559
|
+
* games need at all — so an audio menu meant hand-rolled DOM, and hand-rolled
|
|
3560
|
+
* DOM is out of scene JSON, out of the editor, and out of every headless check.
|
|
3561
|
+
*
|
|
3562
|
+
* It writes the bus; the bus persists itself (`bindAudio`, which
|
|
3563
|
+
* `createGame2D/3D` wire), so there is nothing to save.
|
|
3564
|
+
*/
|
|
3565
|
+
var UiVolumeSlider = class extends UiSlider {
|
|
3566
|
+
static typeName = "UiVolumeSlider";
|
|
3567
|
+
static props = {
|
|
3568
|
+
...UiSlider.props,
|
|
3569
|
+
/** Which bus this row owns. `master` dims everything. */
|
|
3570
|
+
bus: {
|
|
3571
|
+
default: "master",
|
|
3572
|
+
options: [...VOLUME_BUSES]
|
|
3573
|
+
},
|
|
3574
|
+
/** Empty takes the bus's own name — a row nobody labelled still reads. */
|
|
3575
|
+
label: { default: "" }
|
|
3576
|
+
};
|
|
3577
|
+
bus = "master";
|
|
3578
|
+
/** Last value we PUSHED in, to tell an engine-side change from a player drag. */
|
|
3579
|
+
lastPushed = null;
|
|
3580
|
+
_build() {
|
|
3581
|
+
if (!this.label) {
|
|
3582
|
+
const english = VOLUME_LABELS[this.bus] ?? VOLUME_LABELS.master;
|
|
3583
|
+
this.label = translated(this.tree?.engine ?? null, `settings.volume.${this.bus}`, english);
|
|
3584
|
+
}
|
|
3585
|
+
return super._build();
|
|
3586
|
+
}
|
|
3587
|
+
_sync() {
|
|
3588
|
+
const audio = this.tree?.engine?.audio;
|
|
3589
|
+
if (audio) {
|
|
3590
|
+
const stored = audio[this.bus];
|
|
3591
|
+
if (stored !== this.lastPushed) {
|
|
3592
|
+
this.lastPushed = stored;
|
|
3593
|
+
this.value = stored;
|
|
3594
|
+
} else if (this.value !== stored) {
|
|
3595
|
+
audio[this.bus] = this.value;
|
|
3596
|
+
this.lastPushed = audio[this.bus];
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
super._sync();
|
|
3600
|
+
}
|
|
3601
|
+
};
|
|
3602
|
+
/**
|
|
3603
|
+
* The mute switch.
|
|
3604
|
+
*
|
|
3605
|
+
* ```json
|
|
3606
|
+
* { "name": "Mute", "type": "UiMuteToggle" }
|
|
3607
|
+
* ```
|
|
3608
|
+
*
|
|
3609
|
+
* Checked means silent. Like the slider it writes `engine.audio`, which
|
|
3610
|
+
* persists itself.
|
|
3611
|
+
*/
|
|
3612
|
+
var UiMuteToggle = class extends UiToggle {
|
|
3613
|
+
static typeName = "UiMuteToggle";
|
|
3614
|
+
static props = {
|
|
3615
|
+
...UiToggle.props,
|
|
3616
|
+
label: { default: "" }
|
|
3617
|
+
};
|
|
3618
|
+
lastPushed = null;
|
|
3619
|
+
_build() {
|
|
3620
|
+
if (!this.label) this.label = translated(this.tree?.engine ?? null, "settings.mute", "Mute");
|
|
3621
|
+
return super._build();
|
|
3622
|
+
}
|
|
3623
|
+
_sync() {
|
|
3624
|
+
const audio = this.tree?.engine?.audio;
|
|
3625
|
+
if (audio) {
|
|
3626
|
+
if (audio.muted !== this.lastPushed) {
|
|
3627
|
+
this.lastPushed = audio.muted;
|
|
3628
|
+
this.value = audio.muted;
|
|
3629
|
+
} else if (this.value !== audio.muted) {
|
|
3630
|
+
audio.muted = this.value;
|
|
3631
|
+
this.lastPushed = audio.muted;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
super._sync();
|
|
3635
|
+
}
|
|
3636
|
+
};
|
|
3359
3637
|
//#endregion
|
|
3360
3638
|
//#region src/core/register.ts
|
|
3361
3639
|
/**
|
|
@@ -3382,6 +3660,8 @@ function registerCoreNodes() {
|
|
|
3382
3660
|
registerNode(UiQualitySelect);
|
|
3383
3661
|
registerNode(UiFrameCapSelect);
|
|
3384
3662
|
registerNode(UiRenderScaleSelect);
|
|
3663
|
+
registerNode(UiVolumeSlider);
|
|
3664
|
+
registerNode(UiMuteToggle);
|
|
3385
3665
|
}
|
|
3386
3666
|
//#endregion
|
|
3387
|
-
export {
|
|
3667
|
+
export { Localization as A, SFX_PRESET_NAMES as B, qualityCaps as C, suggestQuality as D, readDeviceHints as E, isAudioContextAvailable as F, fadeGain as G, MusicManager as H, ROLLOFF_MODELS as I, AudioBuses as K, spatialGain as L, suggestLocale as M, translationKey as N, LogManager as O, SfxEngine as P, spatialPan as R, Settings as S, qualityRendering as T, WebAudioMusicBackend as U, synthSfx as V, crossfadeGains 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, T_PREFIX as j, BASE_LOCALE as k, UiSelect as l, UiButton as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, UiBar as v, qualityEnvironment as w, Engine as x, UiText as y, SFX_PRESETS as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { b as effectiveOrder, f as Node } from "./loader-
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
1
|
+
import { b as effectiveOrder, f as Node } from "./loader-BTkHYrQn.js";
|
|
2
|
+
import { t as registerCoreNodes } from "./register-BTomIiYG.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
5
|
import { i as getNodeSchema, l as registerNode } from "./registry-C7u42TID.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as behaviorSchema, w as parseNodePath } from "./loader-
|
|
2
|
-
import {
|
|
1
|
+
import { E as behaviorSchema, w as parseNodePath } from "./loader-BTkHYrQn.js";
|
|
2
|
+
import { N as translationKey } from "./register-BTomIiYG.js";
|
|
3
3
|
import { i as getNodeSchema } from "./registry-C7u42TID.js";
|
|
4
4
|
//#region src/core/node-refs.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as diagnose, M as Signal, f as Node, n as loadScene, t as buildNodeJson } from "./loader-
|
|
2
|
-
import {
|
|
1
|
+
import { C as diagnose, M as Signal, f as Node, n as loadScene, t as buildNodeJson } from "./loader-BTkHYrQn.js";
|
|
2
|
+
import { t as registerCoreNodes, x as Engine } from "./register-BTomIiYG.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
5
5
|
import { l as registerNode } from "./registry-C7u42TID.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Dt as Node, Nt as Signal, b as Engine } from "./behavior-
|
|
1
|
+
import { Dt as Node, Nt as Signal, b as Engine } from "./behavior-Do0Da56m.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
3
|
|
|
4
4
|
//#region src/net/types.d.ts
|
|
@@ -161,6 +161,6 @@ function newUid() {
|
|
|
161
161
|
//#endregion
|
|
162
162
|
//#region src/index.ts
|
|
163
163
|
/** Engine version. Kept in sync with package.json by the release pipeline. */
|
|
164
|
-
const VERSION = "0.
|
|
164
|
+
const VERSION = "0.53.0";
|
|
165
165
|
//#endregion
|
|
166
166
|
export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
import { b as AudioPlayer } from "./register-BTomIiYG.js";
|
|
2
|
+
//#region src/core/audio-errors.ts
|
|
3
|
+
/** Every AudioPlayer under `root` whose source could not be played. */
|
|
4
|
+
function audioErrors(root) {
|
|
5
|
+
const out = [];
|
|
6
|
+
const walk = (node) => {
|
|
7
|
+
if (node instanceof AudioPlayer && node.loadError) out.push({
|
|
8
|
+
ref: node.getPath(),
|
|
9
|
+
url: node.src,
|
|
10
|
+
error: node.loadError
|
|
11
|
+
});
|
|
12
|
+
for (const child of node.children) walk(child);
|
|
13
|
+
};
|
|
14
|
+
if (root) walk(root);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/core/canvas-gestures.ts
|
|
19
|
+
/** Give the GAME the touch gestures over its own canvas. */
|
|
20
|
+
function claimCanvasGestures(canvas) {
|
|
21
|
+
const style = canvas?.style;
|
|
22
|
+
if (!style) return;
|
|
23
|
+
style.touchAction = "none";
|
|
24
|
+
style.userSelect = "none";
|
|
25
|
+
style.webkitUserSelect = "none";
|
|
26
|
+
style.webkitTouchCallout = "none";
|
|
27
|
+
style.webkitTapHighlightColor = "transparent";
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
1
30
|
//#region src/core/editor-switch.ts
|
|
2
31
|
/**
|
|
3
32
|
* Cover the page while it changes what it is.
|
|
@@ -295,4 +324,4 @@ function defaultReport(what, error) {
|
|
|
295
324
|
console.error(`[incanto] teardown: ${what} failed (continuing)`, error);
|
|
296
325
|
}
|
|
297
326
|
//#endregion
|
|
298
|
-
export { openBundledEditor as a, devServerLibrary as i, pauseWhenHidden as n, poseFromRenderer as o, crossFade as r, teardown as t };
|
|
327
|
+
export { openBundledEditor as a, audioErrors as c, devServerLibrary as i, pauseWhenHidden as n, poseFromRenderer as o, crossFade as r, claimCanvasGestures as s, teardown as t };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { A as registerBehavior, n as loadScene, s as resolveViewport } from "./loader-
|
|
2
|
-
import {
|
|
1
|
+
import { A as registerBehavior, n as loadScene, s as resolveViewport } from "./loader-BTkHYrQn.js";
|
|
2
|
+
import { x as Engine } from "./register-BTomIiYG.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
5
5
|
import { i as getNodeSchema, s as mergeStaticProps } from "./registry-C7u42TID.js";
|
|
6
|
-
import { n as startRecording } from "./replay-
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
-
import { t as registerNodes2D } from "./register-
|
|
9
|
-
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-
|
|
10
|
-
import { n as registerNodesNet, t as createSplitScreen } from "./split-screen-
|
|
6
|
+
import { n as startRecording } from "./replay-DYNUL4BU.js";
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-CRYw_Q-T.js";
|
|
8
|
+
import { t as registerNodes2D } from "./register-DROK2l7J.js";
|
|
9
|
+
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-SkGanr2s.js";
|
|
10
|
+
import { n as registerNodesNet, t as createSplitScreen } from "./split-screen-CaF9hO7g.js";
|
|
11
11
|
import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
12
12
|
//#region src/test/framing.ts
|
|
13
13
|
/**
|
|
@@ -1718,10 +1718,10 @@ async function runScript(json, opts) {
|
|
|
1718
1718
|
engine.setScene(scene);
|
|
1719
1719
|
const physics = opts.physics ?? "auto";
|
|
1720
1720
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1721
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
1721
|
+
const { enablePhysics2D } = await import("./physics-2d-BhZZ-HAp.js").then((n) => n.r);
|
|
1722
1722
|
await enablePhysics2D(engine);
|
|
1723
1723
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1724
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
1724
|
+
const { enablePhysics3D } = await import("./physics-3d-DmyO2oaN.js").then((n) => n.r);
|
|
1725
1725
|
await enablePhysics3D(engine);
|
|
1726
1726
|
}
|
|
1727
1727
|
const failures = [];
|
|
@@ -1834,10 +1834,10 @@ async function createPlaySession(json, opts = {}) {
|
|
|
1834
1834
|
engine.setScene(scene);
|
|
1835
1835
|
const physics = opts.physics ?? "auto";
|
|
1836
1836
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
1837
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
1837
|
+
const { enablePhysics2D } = await import("./physics-2d-BhZZ-HAp.js").then((n) => n.r);
|
|
1838
1838
|
await enablePhysics2D(engine);
|
|
1839
1839
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
1840
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
1840
|
+
const { enablePhysics3D } = await import("./physics-3d-DmyO2oaN.js").then((n) => n.r);
|
|
1841
1841
|
await enablePhysics3D(engine);
|
|
1842
1842
|
}
|
|
1843
1843
|
const stepMs = 1e3 / (opts.fixedHz ?? 60);
|
package/dist/test.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { At as LogEntry, Dt as Node, P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-
|
|
1
|
+
import { At as LogEntry, Dt as Node, P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-Do0Da56m.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
4
|
-
import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-
|
|
5
|
-
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-Cu_7kJDy.js";
|
|
4
|
+
import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-9Fy6C10F.js";
|
|
5
|
+
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-DNmcX1Pz.js";
|
|
6
6
|
|
|
7
7
|
//#region src/test/facing.d.ts
|
|
8
8
|
interface FacingOptions {
|
package/dist/test.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { r as auditScene } from "./replay-
|
|
2
|
-
import { S as framingText, _ as failingReplays, a as registerAllNodes, b as playtestText, c as ladderText, d as multiplayText, f as playMultiplayer, g as facingText, h as facingReport, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as findPlayer, x as describeFraming, y as playtest } from "./test-
|
|
1
|
+
import { r as auditScene } from "./replay-DYNUL4BU.js";
|
|
2
|
+
import { S as framingText, _ as failingReplays, a as registerAllNodes, b as playtestText, c as ladderText, d as multiplayText, f as playMultiplayer, g as facingText, h as facingReport, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as findPlayer, x as describeFraming, y as playtest } from "./test-CJRsciFk.js";
|
|
3
3
|
export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, validateScene };
|
|
@@ -80,8 +80,8 @@ var TouchControls = class {
|
|
|
80
80
|
const base = doc.createElement("div");
|
|
81
81
|
style(base, {
|
|
82
82
|
position: "absolute",
|
|
83
|
-
left:
|
|
84
|
-
bottom: "24px",
|
|
83
|
+
left: `calc(${24 + index * 140}px + env(safe-area-inset-left))`,
|
|
84
|
+
bottom: "calc(24px + env(safe-area-inset-bottom))",
|
|
85
85
|
width: "120px",
|
|
86
86
|
height: "120px",
|
|
87
87
|
borderRadius: "50%",
|
|
@@ -150,8 +150,8 @@ var TouchControls = class {
|
|
|
150
150
|
button.textContent = action;
|
|
151
151
|
style(button, {
|
|
152
152
|
position: "absolute",
|
|
153
|
-
right: "24px",
|
|
154
|
-
bottom:
|
|
153
|
+
right: "calc(24px + env(safe-area-inset-right))",
|
|
154
|
+
bottom: `calc(${24 + index * 80}px + env(safe-area-inset-bottom))`,
|
|
155
155
|
width: "64px",
|
|
156
156
|
height: "64px",
|
|
157
157
|
borderRadius: "50%",
|
package/dist/vite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as VERSION } from "./src-
|
|
1
|
+
import { t as VERSION } from "./src-C9xyZW7M.js";
|
|
2
2
|
import { n as diffSignatures } from "./frame-report-njybhZon.js";
|
|
3
|
-
import { s as validateScene } from "./test-
|
|
3
|
+
import { s as validateScene } from "./test-CJRsciFk.js";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, normalize, relative, resolve, sep } from "node:path";
|
|
6
6
|
//#region src/vite/frame-endpoint.ts
|