incanto 0.58.0 → 0.59.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-multiplay.mjs +170 -0
- package/bin/incanto-verify.mjs +68 -4
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +8 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-l08AEbq9.d.ts → behavior-DWKTUzKI.d.ts} +10 -0
- package/dist/{create-game-D10bU5_J.js → create-game-BiW8Men_.js} +6 -6
- package/dist/{create-game-BZwWJIns.js → create-game-CHDLDQsQ.js} +6 -6
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -2
- package/dist/{duplicate-BPLZDZpd.js → duplicate-DJQd44CD.js} +1 -1
- package/dist/{environment-presets-CvvQr_bJ.js → environment-presets-DRAz5EV9.js} +12 -10
- package/dist/{gameplay-BVphcxmE.js → gameplay-BBEjPFsR.js} +62 -34
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +36 -8
- package/dist/index.js +8 -8
- package/dist/{json-BLk7H2Qa.js → json-CwwhxQgb.js} +7 -1
- package/dist/{loader-BcrRSjxB.js → loader-D8n7TU8W.js} +142 -5
- package/dist/{loader-BbEMTuWg.d.ts → loader-TvkRFbyL.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{pathfinding-mEN4V1CU.d.ts → pathfinding-BqWBb0kh.d.ts} +1 -1
- package/dist/{physics-2d-EqA-jddf.js → physics-2d-BaRSRrrZ.js} +12 -2
- package/dist/{physics-3d-Dnz4fsXX.js → physics-3d-CYxjh-HW.js} +13 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/{register-C6ZBFRjd.js → register-BpFcgdcL.js} +57 -27
- package/dist/{register-Ch70uByv.js → register-CDrAQqPp.js} +90 -41
- package/dist/{registry-C7u42TID.js → registry-WWcQcfMr.js} +1 -1
- package/dist/{replay-s7I2GstT.js → replay-CEPyQtF_.js} +31 -7
- package/dist/{replay-Dw6gMlYA.d.ts → replay-O-yAGM76.d.ts} +1 -1
- package/dist/{split-screen-B0baBwxI.d.ts → split-screen-BQ3tAsf-.d.ts} +38 -1
- package/dist/{split-screen-DLsUrleX.js → split-screen-DDMZutQ6.js} +56 -13
- package/dist/{sprite-animation-C0wXLBZJ.js → sprite-animation-CY-mrr1L.js} +1 -1
- package/dist/{src-C1J09Op6.js → src-CY21B462.js} +1 -1
- package/dist/{teardown-Cs113S9F.js → teardown-RApWnM1G.js} +1 -1
- package/dist/{test-ZBga8kQ9.js → test-DHYuFyAu.js} +260 -25
- package/dist/test.d.ts +103 -5
- package/dist/test.js +3 -3
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-BQQjE9UQ.js → agent8-BoRGtVxK.js} +1 -1
- package/editor/assets/{debug-C9UCsXBS.js → debug-CzdyCg75.js} +1 -1
- package/editor/assets/{index-CBgfM3WD.js → index-VesuVEhe.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +2 -1
- package/schemas/scene.schema.json +1174 -70
- package/skills/incanto-localization.md +40 -8
- package/skills/incanto-multiplayer.md +57 -4
- package/skills/incanto-node-reference.md +18 -0
- package/skills/incanto-physics-and-input.md +26 -0
- package/skills/incanto-verifying-your-game.md +22 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { C as Node, N as Signal, S as InputMap, g as restoreBehaviors, h as captureBehaviors, v as createSaveStore, w as diagnose, x as resolveOrderGroups, y as ORDER_GROUP_BASE } from "./loader-D8n7TU8W.js";
|
|
2
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
4
|
-
import { l as registerNode } from "./registry-
|
|
5
|
+
import { l as registerNode } from "./registry-WWcQcfMr.js";
|
|
5
6
|
//#region src/core/audio/buses.ts
|
|
6
7
|
const AUDIO_LOG_LIMIT = 200;
|
|
7
8
|
var AudioBuses = class {
|
|
@@ -1160,6 +1161,18 @@ var Localization = class {
|
|
|
1160
1161
|
const key = translationKey(value);
|
|
1161
1162
|
return key === null ? value : this.t(key, params);
|
|
1162
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Whether the ACTIVE locale or the base declares this key.
|
|
1166
|
+
*
|
|
1167
|
+
* This is the runtime question, and it is not the same as `has`: a key only
|
|
1168
|
+
* `ko` declares is "declared", and an English player still gets the raw key
|
|
1169
|
+
* back from `t()`, because the fallback chain is current → base → the key.
|
|
1170
|
+
* Widgets that ask before translating asked the wrong one and painted
|
|
1171
|
+
* `settings.quality.low` into an English menu.
|
|
1172
|
+
*/
|
|
1173
|
+
declares(key) {
|
|
1174
|
+
return this.tables[this.current]?.[key] !== void 0 || this.tables["en"]?.[key] !== void 0;
|
|
1175
|
+
}
|
|
1163
1176
|
/** Whether any locale declares this key — for the audit, not for the runtime. */
|
|
1164
1177
|
has(key) {
|
|
1165
1178
|
for (const entries of Object.values(this.tables)) if (entries[key] !== void 0) return true;
|
|
@@ -1182,6 +1195,22 @@ function suggestLocale(available, languages) {
|
|
|
1182
1195
|
}
|
|
1183
1196
|
return "en";
|
|
1184
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Resolve a text prop on ANY node, not just a DOM HUD widget.
|
|
1200
|
+
*
|
|
1201
|
+
* `@t:` was honoured in exactly one place — `HudWidgetBase._t` — so text that
|
|
1202
|
+
* lives in the WORLD had no localization path at all: a `Label3D` reading
|
|
1203
|
+
* `"@t:sign.welcome"` rasterized those literal characters into the texture
|
|
1204
|
+
* standing over the shop door, in every language, and the audit certified the
|
|
1205
|
+
* scene clean because the key WAS declared.
|
|
1206
|
+
*
|
|
1207
|
+
* Returns the value untouched when there is no tree yet or no marker, so it is
|
|
1208
|
+
* safe to call from a rasterize path that runs before the node is in a scene.
|
|
1209
|
+
*/
|
|
1210
|
+
function translateOn(node, value) {
|
|
1211
|
+
const locale = node.tree?.engine?.locale;
|
|
1212
|
+
return locale ? locale.resolve(value) : value;
|
|
1213
|
+
}
|
|
1185
1214
|
//#endregion
|
|
1186
1215
|
//#region src/core/nodes/hud.ts
|
|
1187
1216
|
const ANCHOR_CSS = {
|
|
@@ -1536,6 +1565,33 @@ var HudWidgetBase = class extends Node {
|
|
|
1536
1565
|
const locale = this.tree?.engine?.locale;
|
|
1537
1566
|
return locale ? locale.resolve(value) : value;
|
|
1538
1567
|
}
|
|
1568
|
+
/**
|
|
1569
|
+
* A translation with an English wording to fall back to.
|
|
1570
|
+
*
|
|
1571
|
+
* For text the ENGINE supplies — "Music", "Mute", "Unlimited" — where there
|
|
1572
|
+
* is no author string to carry a `@t:` marker. Asks `declares()`, not
|
|
1573
|
+
* `has()`: a key only another locale declares would otherwise come back as
|
|
1574
|
+
* the raw key, which is worse than the English.
|
|
1575
|
+
*/
|
|
1576
|
+
/**
|
|
1577
|
+
* What this widget's label READS as — resolved every frame, never stored.
|
|
1578
|
+
*
|
|
1579
|
+
* `UiVolumeSlider` and `UiMuteToggle` used to compute their auto-label in
|
|
1580
|
+
* `_build()` and write the result back into `this.label`, which broke them
|
|
1581
|
+
* twice over: the per-frame `_t(this.label)` then had a plain string with no
|
|
1582
|
+
* marker left to resolve, and `_build` runs from `onReady` — inside
|
|
1583
|
+
* `loadScene`, BEFORE `setScene` merges the scene's `strings` — so the table
|
|
1584
|
+
* was empty at bake time and the English was permanent even for a game that
|
|
1585
|
+
* boots in another language.
|
|
1586
|
+
*/
|
|
1587
|
+
_labelText() {
|
|
1588
|
+
const label = this.label;
|
|
1589
|
+
return typeof label === "string" ? this._t(label) : "";
|
|
1590
|
+
}
|
|
1591
|
+
_tOr(key, fallback) {
|
|
1592
|
+
const locale = this.tree?.engine?.locale;
|
|
1593
|
+
return locale?.declares(key) ? locale.t(key) : fallback;
|
|
1594
|
+
}
|
|
1539
1595
|
};
|
|
1540
1596
|
/** A text line (score, timer, hints). Set `.text` from behaviors. */
|
|
1541
1597
|
var UiText = class extends HudWidgetBase {
|
|
@@ -2126,7 +2182,7 @@ var Settings = class {
|
|
|
2126
2182
|
* without knowing this class exists. Returns the unsubscribe.
|
|
2127
2183
|
*/
|
|
2128
2184
|
bindLocale(locale) {
|
|
2129
|
-
locale.locale = this.
|
|
2185
|
+
locale.locale = this.store.get("language", "") || suggestLocale(locale.available());
|
|
2130
2186
|
return locale.changed.connect((next) => {
|
|
2131
2187
|
this.set("language", next);
|
|
2132
2188
|
});
|
|
@@ -2229,6 +2285,7 @@ function qualityRendering(tier) {
|
|
|
2229
2285
|
}
|
|
2230
2286
|
//#endregion
|
|
2231
2287
|
//#region src/core/engine.ts
|
|
2288
|
+
var engine_exports = /* @__PURE__ */ __exportAll({ Engine: () => Engine });
|
|
2232
2289
|
const MAX_DT_SECONDS = .25;
|
|
2233
2290
|
function rafScheduler(cb) {
|
|
2234
2291
|
let live = true;
|
|
@@ -3193,17 +3250,6 @@ function px(n) {
|
|
|
3193
3250
|
return `${n}px`;
|
|
3194
3251
|
}
|
|
3195
3252
|
/**
|
|
3196
|
-
* The translated string, or the English one.
|
|
3197
|
-
*
|
|
3198
|
-
* `locale.t` returns the KEY when nothing declares it, and a menu reading
|
|
3199
|
-
* `settings.volume.music` is worse than one reading `Music`. English is the
|
|
3200
|
-
* base language and the fallback, always.
|
|
3201
|
-
*/
|
|
3202
|
-
function translated(engine, key, fallback) {
|
|
3203
|
-
const locale = engine?.locale;
|
|
3204
|
-
return locale?.has(key) ? locale.t(key) : fallback;
|
|
3205
|
-
}
|
|
3206
|
-
/**
|
|
3207
3253
|
* A box that holds other widgets — the thing a menu actually is.
|
|
3208
3254
|
*
|
|
3209
3255
|
* Widgets normally mount into their HudLayer's anchor slot, which is why a
|
|
@@ -3377,7 +3423,7 @@ var UiSlider = class extends HudWidgetBase {
|
|
|
3377
3423
|
const wrap = document.createElement("div");
|
|
3378
3424
|
wrap.style.cssText = "display:flex;align-items:center;gap:8px;pointer-events:auto;font:600 13px system-ui,-apple-system,'Segoe UI',sans-serif;color:#fff;";
|
|
3379
3425
|
const text = document.createElement("span");
|
|
3380
|
-
text.textContent = this.
|
|
3426
|
+
text.textContent = this._labelText();
|
|
3381
3427
|
const input = document.createElement("input");
|
|
3382
3428
|
input.type = "range";
|
|
3383
3429
|
input.min = String(this.min);
|
|
@@ -3390,7 +3436,7 @@ var UiSlider = class extends HudWidgetBase {
|
|
|
3390
3436
|
this.last = this.value;
|
|
3391
3437
|
this.emit("changed", this.value);
|
|
3392
3438
|
});
|
|
3393
|
-
if (this.
|
|
3439
|
+
if (this._labelText()) wrap.appendChild(text);
|
|
3394
3440
|
wrap.appendChild(input);
|
|
3395
3441
|
this.input = input;
|
|
3396
3442
|
this.text = text;
|
|
@@ -3402,7 +3448,7 @@ var UiSlider = class extends HudWidgetBase {
|
|
|
3402
3448
|
this.last = this.value;
|
|
3403
3449
|
this.input.value = String(this.value);
|
|
3404
3450
|
}
|
|
3405
|
-
const label = this.
|
|
3451
|
+
const label = this._labelText();
|
|
3406
3452
|
if (this.text && this.text.textContent !== label) this.text.textContent = label;
|
|
3407
3453
|
}
|
|
3408
3454
|
};
|
|
@@ -3442,9 +3488,9 @@ var UiToggle = class extends HudWidgetBase {
|
|
|
3442
3488
|
this.emit("changed", this.value);
|
|
3443
3489
|
});
|
|
3444
3490
|
const text = document.createElement("span");
|
|
3445
|
-
text.textContent = this.
|
|
3491
|
+
text.textContent = this._labelText();
|
|
3446
3492
|
this.tag = text;
|
|
3447
|
-
this.lastLabel = this.
|
|
3493
|
+
this.lastLabel = this._labelText();
|
|
3448
3494
|
wrap.append(input, text);
|
|
3449
3495
|
this.input = input;
|
|
3450
3496
|
this.last = this.value;
|
|
@@ -3457,7 +3503,7 @@ var UiToggle = class extends HudWidgetBase {
|
|
|
3457
3503
|
this.last = this.value;
|
|
3458
3504
|
this.input.checked = this.value;
|
|
3459
3505
|
}
|
|
3460
|
-
const label = this.
|
|
3506
|
+
const label = this._labelText();
|
|
3461
3507
|
if (this.tag && label !== this.lastLabel) {
|
|
3462
3508
|
this.lastLabel = label;
|
|
3463
3509
|
this.tag.textContent = label;
|
|
@@ -3507,24 +3553,33 @@ var UiSelect = class extends HudWidgetBase {
|
|
|
3507
3553
|
this.last = this.value;
|
|
3508
3554
|
this.emit("changed", this.value);
|
|
3509
3555
|
});
|
|
3510
|
-
if (this.
|
|
3556
|
+
if (this._labelText()) wrap.appendChild(text);
|
|
3511
3557
|
wrap.appendChild(select);
|
|
3512
3558
|
this.select = select;
|
|
3513
3559
|
this.fillOptions();
|
|
3514
3560
|
return wrap;
|
|
3515
3561
|
}
|
|
3516
|
-
/**
|
|
3562
|
+
/**
|
|
3563
|
+
* What an option READS as, when it differs from its value.
|
|
3564
|
+
*
|
|
3565
|
+
* The base resolves `@t:` like every other text prop: the VALUE stays exactly
|
|
3566
|
+
* what the author wrote — so a `changed` handler never has to know the active
|
|
3567
|
+
* language — and only the displayed words change. Without this a dropdown was
|
|
3568
|
+
* the one widget with no way to speak anything but English, and
|
|
3569
|
+
* `"options": "@t:diff.easy"` rendered that marker literally on screen.
|
|
3570
|
+
*/
|
|
3517
3571
|
_optionLabel(value) {
|
|
3518
|
-
return value;
|
|
3572
|
+
return this._t(value);
|
|
3519
3573
|
}
|
|
3520
3574
|
fillOptions() {
|
|
3521
3575
|
const select = this.select;
|
|
3522
|
-
if (!select
|
|
3523
|
-
|
|
3576
|
+
if (!select) return;
|
|
3577
|
+
const values = this.options.split(",").map((o) => o.trim()).filter(Boolean);
|
|
3578
|
+
const painted = values.map((v) => `${v}\u0000${this._optionLabel(v)}`).join("");
|
|
3579
|
+
if (painted === this.lastOptions) return;
|
|
3580
|
+
this.lastOptions = painted;
|
|
3524
3581
|
select.textContent = "";
|
|
3525
|
-
for (const
|
|
3526
|
-
const value = raw.trim();
|
|
3527
|
-
if (!value) continue;
|
|
3582
|
+
for (const value of values) {
|
|
3528
3583
|
const option = document.createElement("option");
|
|
3529
3584
|
option.value = value;
|
|
3530
3585
|
option.textContent = this._optionLabel(value);
|
|
@@ -3635,9 +3690,6 @@ var UiSettingSelect = class extends UiSelect {
|
|
|
3635
3690
|
* `settings.quality.high` is worse than one reading `High`. English is the
|
|
3636
3691
|
* base language and the fallback, always.
|
|
3637
3692
|
*/
|
|
3638
|
-
_tOr(key, fallback) {
|
|
3639
|
-
return translated(this._engine(), key, fallback);
|
|
3640
|
-
}
|
|
3641
3693
|
_sync() {
|
|
3642
3694
|
const engine = this._engine();
|
|
3643
3695
|
if (engine) {
|
|
@@ -3807,12 +3859,10 @@ var UiVolumeSlider = class extends UiSlider {
|
|
|
3807
3859
|
bus = "master";
|
|
3808
3860
|
/** Last value we PUSHED in, to tell an engine-side change from a player drag. */
|
|
3809
3861
|
lastPushed = null;
|
|
3810
|
-
|
|
3811
|
-
if (
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
}
|
|
3815
|
-
return super._build();
|
|
3862
|
+
_labelText() {
|
|
3863
|
+
if (this.label) return this._t(this.label);
|
|
3864
|
+
const english = VOLUME_LABELS[this.bus] ?? VOLUME_LABELS.master;
|
|
3865
|
+
return this._tOr(`settings.volume.${this.bus}`, english);
|
|
3816
3866
|
}
|
|
3817
3867
|
_sync() {
|
|
3818
3868
|
const audio = this.tree?.engine?.audio;
|
|
@@ -3846,9 +3896,8 @@ var UiMuteToggle = class extends UiToggle {
|
|
|
3846
3896
|
label: { default: "" }
|
|
3847
3897
|
};
|
|
3848
3898
|
lastPushed = null;
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
return super._build();
|
|
3899
|
+
_labelText() {
|
|
3900
|
+
return this.label ? this._t(this.label) : this._tOr("settings.mute", "Mute");
|
|
3852
3901
|
}
|
|
3853
3902
|
_sync() {
|
|
3854
3903
|
const audio = this.tree?.engine?.audio;
|
|
@@ -3894,4 +3943,4 @@ function registerCoreNodes() {
|
|
|
3894
3943
|
registerNode(UiMuteToggle);
|
|
3895
3944
|
}
|
|
3896
3945
|
//#endregion
|
|
3897
|
-
export {
|
|
3946
|
+
export { UiBar as A, ROLLOFF_MODELS as B, readDeviceHints as C, HudLayer as D, EffectLog as E, suggestLocale as F, synthSfx as G, spatialPan as H, translateOn as I, crossfadeGains as J, MusicManager as K, translationKey as L, BASE_LOCALE as M, Localization as N, HudWidgetBase as O, T_PREFIX as P, SfxEngine as R, qualityRendering as S, LogManager as T, SFX_PRESETS as U, spatialGain as V, SFX_PRESET_NAMES as W, AudioBuses as X, fadeGain as Y, Engine as _, UiMuteToggle as a, qualityCaps as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, AudioPlayer as g, UiDialogue as h, UiLanguageSelect as i, UiText as j, UiBanner as k, UiSelect as l, UiButton as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, WebAudioMusicBackend as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, engine_exports as v, suggestQuality as w, qualityEnvironment as x, Settings as y, isAudioContextAvailable as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
-
import { r as jsonKind, t as jsonClone } from "./json-
|
|
2
|
+
import { r as jsonKind, t as jsonClone } from "./json-CwwhxQgb.js";
|
|
3
3
|
//#region src/core/registry.ts
|
|
4
4
|
/**
|
|
5
5
|
* Validate a prop value against its schema default:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { i as getNodeSchema, o as getNodeType } from "./registry-
|
|
1
|
+
import { D as behaviorSchema, T as parseNodePath } from "./loader-D8n7TU8W.js";
|
|
2
|
+
import { L as translationKey, O as HudWidgetBase } from "./register-CDrAQqPp.js";
|
|
3
|
+
import { i as getNodeSchema, o as getNodeType } from "./registry-WWcQcfMr.js";
|
|
4
4
|
//#region src/core/node-refs.ts
|
|
5
5
|
/**
|
|
6
6
|
* Node-path PROPS, resolved against scene JSON.
|
|
@@ -317,15 +317,28 @@ function auditScene(scene) {
|
|
|
317
317
|
* screen. Reported here — at author time, where it can still be fixed — rather
|
|
318
318
|
* than at runtime in front of someone playing.
|
|
319
319
|
*/
|
|
320
|
+
/**
|
|
321
|
+
* Close to `@t:` without being it — `@T:`, a leading space, `@t :`, `@t.`.
|
|
322
|
+
* Deliberately narrow: an at-sign in ordinary prose ("email me @ home") must
|
|
323
|
+
* not trip it.
|
|
324
|
+
*/
|
|
325
|
+
const NEAR_MISS = /^\s*@\s*t\s*[:.]/i;
|
|
320
326
|
function auditStrings(scene) {
|
|
321
327
|
const tables = scene.strings;
|
|
322
328
|
const declared = /* @__PURE__ */ new Set();
|
|
323
329
|
for (const entries of Object.values(tables ?? {})) for (const key of Object.keys(entries)) declared.add(key);
|
|
330
|
+
const inBase = new Set(Object.keys(tables?.["en"] ?? {}));
|
|
324
331
|
const missing = /* @__PURE__ */ new Set();
|
|
332
|
+
const notInEnglish = /* @__PURE__ */ new Set();
|
|
333
|
+
const nearMiss = /* @__PURE__ */ new Set();
|
|
325
334
|
const walk = (value) => {
|
|
326
335
|
if (typeof value === "string") {
|
|
336
|
+
if (translationKey(value) === null && NEAR_MISS.test(value)) nearMiss.add(value);
|
|
327
337
|
const key = translationKey(value);
|
|
328
|
-
if (key !== null
|
|
338
|
+
if (key !== null) {
|
|
339
|
+
if (!declared.has(key)) missing.add(key);
|
|
340
|
+
else if (tables && !inBase.has(key)) notInEnglish.add(key);
|
|
341
|
+
}
|
|
329
342
|
return;
|
|
330
343
|
}
|
|
331
344
|
if (Array.isArray(value)) {
|
|
@@ -335,9 +348,20 @@ function auditStrings(scene) {
|
|
|
335
348
|
if (value !== null && typeof value === "object") for (const item of Object.values(value)) walk(item);
|
|
336
349
|
};
|
|
337
350
|
walk(scene.root);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
const out = [];
|
|
352
|
+
if (missing.size > 0) {
|
|
353
|
+
const listed = [...missing].sort();
|
|
354
|
+
out.push(`${listed.length} translation key${listed.length === 1 ? "" : "s"} nothing declares: ${listed.join(", ")}. The key itself renders on screen. Add them under "strings": { "en": { ... } } — other locales may omit any of them and fall back to English.`);
|
|
355
|
+
}
|
|
356
|
+
if (nearMiss.size > 0) {
|
|
357
|
+
const listed = [...nearMiss].sort();
|
|
358
|
+
out.push(`${listed.length} value${listed.length === 1 ? "" : "s"} look${listed.length === 1 ? "s" : ""} like a translation marker but ${listed.length === 1 ? "is" : "are"} not: ${listed.map((v) => JSON.stringify(v)).join(", ")}. It must be exactly "@t:key" — lower-case, no space. As written, the text itself renders on screen.`);
|
|
359
|
+
}
|
|
360
|
+
if (notInEnglish.size > 0) {
|
|
361
|
+
const listed = [...notInEnglish].sort();
|
|
362
|
+
out.push(`${listed.length} translation key${listed.length === 1 ? "" : "s"} missing from "en": ${listed.join(", ")}. English is the base every other locale falls back to, so these render as the raw key for anyone not using the locale that declares them.`);
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
341
365
|
}
|
|
342
366
|
//#endregion
|
|
343
367
|
//#region src/core/replay.ts
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { At as Node, It as Signal, b as Engine } from "./behavior-
|
|
1
|
+
import { At as Node, It as Signal, b as Engine } from "./behavior-DWKTUzKI.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
|
|
3
4
|
|
|
4
5
|
//#region src/net/types.d.ts
|
|
5
6
|
type Unsubscribe = () => void;
|
|
@@ -277,6 +278,17 @@ interface NetworkManagerOptions {
|
|
|
277
278
|
};
|
|
278
279
|
/** Replication send window in ms (default 50; node `network.throttleMs` can lower it). */
|
|
279
280
|
throttleMs?: number;
|
|
281
|
+
/**
|
|
282
|
+
* How often the FULL sync set is re-sent regardless of what changed, in ms
|
|
283
|
+
* (default 2000; 0 disables).
|
|
284
|
+
*
|
|
285
|
+
* Deltas are right on the wire and wrong in the store: backends shallow-merge
|
|
286
|
+
* one level down, so each send replaces the whole `sync` object and a key
|
|
287
|
+
* that stopped changing is erased from the authoritative state. A keyframe
|
|
288
|
+
* puts it back — for the next joiner, and for a send that never reached the
|
|
289
|
+
* wire, which is never retried.
|
|
290
|
+
*/
|
|
291
|
+
keyframeMs?: number;
|
|
280
292
|
}
|
|
281
293
|
/**
|
|
282
294
|
* Per-engine multiplayer hub: joins a room, exposes the room channels as core
|
|
@@ -308,6 +320,7 @@ declare class NetworkManager {
|
|
|
308
320
|
private readonly server;
|
|
309
321
|
private readonly engine;
|
|
310
322
|
private readonly throttleMs;
|
|
323
|
+
private readonly keyframeMs;
|
|
311
324
|
private readonly subs;
|
|
312
325
|
private readonly messageSignals;
|
|
313
326
|
private readonly collectionSignals;
|
|
@@ -320,8 +333,23 @@ declare class NetworkManager {
|
|
|
320
333
|
/** Sync keys already reported unreadable, so a per-frame send says it once. */
|
|
321
334
|
private readonly reportedDeadKeys;
|
|
322
335
|
/** Name a sync key the owner cannot read — a typo in the prop or the path. */
|
|
336
|
+
/**
|
|
337
|
+
* A sync key that has NEVER been readable, reported once — after a grace
|
|
338
|
+
* period.
|
|
339
|
+
*
|
|
340
|
+
* Behaviours set their props in `onReady`/`update`, and replication runs on
|
|
341
|
+
* the fixed step, so a perfectly correct key reads as `undefined` on the
|
|
342
|
+
* first pass or two. Reporting immediately made the engine's own co-op
|
|
343
|
+
* example print "sync key 'firing' … it is never sent" about a key that is
|
|
344
|
+
* sent from frame 2 onward — a diagnostic that cries wolf teaches its reader
|
|
345
|
+
* to ignore the one that does not.
|
|
346
|
+
*/
|
|
323
347
|
private reportDeadSyncKey;
|
|
324
348
|
private sendAccumulator;
|
|
349
|
+
/** Simulated ms since the last time the FULL sync set went out. */
|
|
350
|
+
private keyframeAccumulator;
|
|
351
|
+
/** Simulated ms this manager has been replicating — see reportDeadSyncKey. */
|
|
352
|
+
private aliveMs;
|
|
325
353
|
private detachReplication;
|
|
326
354
|
private lastOwner;
|
|
327
355
|
private boundScene;
|
|
@@ -399,6 +427,15 @@ interface SplitScreenOptions extends LocalGameServerOptions {
|
|
|
399
427
|
/** Engine seed base (each panel gets seed + index). */
|
|
400
428
|
seed?: number;
|
|
401
429
|
/**
|
|
430
|
+
* Loader options, per panel — `stubMissingBehaviors` for a headless check
|
|
431
|
+
* that has no TypeScript, `resolveScene` for `instance:` sub-scenes.
|
|
432
|
+
*
|
|
433
|
+
* Without these, `createSplitScreen` hard-failed on any real game: it called
|
|
434
|
+
* `loadScene` with nothing registered, so a scene naming one of its own
|
|
435
|
+
* behaviours threw `Unknown behavior` before a single frame ran.
|
|
436
|
+
*/
|
|
437
|
+
load?: LoadSceneOptions;
|
|
438
|
+
/**
|
|
402
439
|
* The room every panel joins. Default: whatever the FIRST panel got.
|
|
403
440
|
*
|
|
404
441
|
* A shared scene usually says `multiplayer: { room: "auto" }`, and "auto"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { _ as Engine, t as registerCoreNodes } from "./register-
|
|
1
|
+
import { C as Node, N as Signal, n as loadScene, t as buildNodeJson, w as diagnose } from "./loader-D8n7TU8W.js";
|
|
2
|
+
import { _ as Engine, t as registerCoreNodes } from "./register-CDrAQqPp.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
|
-
import { n as jsonEquals, t as jsonClone } from "./json-
|
|
5
|
-
import { l as registerNode } from "./registry-
|
|
4
|
+
import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
|
|
5
|
+
import { l as registerNode } from "./registry-WWcQcfMr.js";
|
|
6
6
|
//#region src/net/loopback.ts
|
|
7
7
|
/**
|
|
8
8
|
* In-memory multiplayer hub implementing the SAME kernel contract as
|
|
@@ -771,6 +771,15 @@ var LocalGameServerTransport = class {
|
|
|
771
771
|
};
|
|
772
772
|
//#endregion
|
|
773
773
|
//#region src/net/network-manager.ts
|
|
774
|
+
/**
|
|
775
|
+
* How long a sync key may read as `undefined` before it is called dead.
|
|
776
|
+
*
|
|
777
|
+
* Long enough for `onReady` and a few `update`s to have run; short enough that
|
|
778
|
+
* a genuinely wrong prop name is named while its author is still looking.
|
|
779
|
+
*/
|
|
780
|
+
const DEAD_KEY_GRACE_MS = 500;
|
|
781
|
+
/** Full-state resend interval, in simulated ms. See `keyframeMs`. */
|
|
782
|
+
const DEFAULT_KEYFRAME_MS = 2e3;
|
|
774
783
|
const managers = /* @__PURE__ */ new WeakMap();
|
|
775
784
|
/**
|
|
776
785
|
* Per-engine multiplayer hub: joins a room, exposes the room channels as core
|
|
@@ -802,6 +811,7 @@ var NetworkManager = class NetworkManager {
|
|
|
802
811
|
server;
|
|
803
812
|
engine;
|
|
804
813
|
throttleMs;
|
|
814
|
+
keyframeMs;
|
|
805
815
|
subs = [];
|
|
806
816
|
messageSignals = /* @__PURE__ */ new Map();
|
|
807
817
|
collectionSignals = /* @__PURE__ */ new Map();
|
|
@@ -814,12 +824,28 @@ var NetworkManager = class NetworkManager {
|
|
|
814
824
|
/** Sync keys already reported unreadable, so a per-frame send says it once. */
|
|
815
825
|
reportedDeadKeys = /* @__PURE__ */ new Set();
|
|
816
826
|
/** Name a sync key the owner cannot read — a typo in the prop or the path. */
|
|
827
|
+
/**
|
|
828
|
+
* A sync key that has NEVER been readable, reported once — after a grace
|
|
829
|
+
* period.
|
|
830
|
+
*
|
|
831
|
+
* Behaviours set their props in `onReady`/`update`, and replication runs on
|
|
832
|
+
* the fixed step, so a perfectly correct key reads as `undefined` on the
|
|
833
|
+
* first pass or two. Reporting immediately made the engine's own co-op
|
|
834
|
+
* example print "sync key 'firing' … it is never sent" about a key that is
|
|
835
|
+
* sent from frame 2 onward — a diagnostic that cries wolf teaches its reader
|
|
836
|
+
* to ignore the one that does not.
|
|
837
|
+
*/
|
|
817
838
|
reportDeadSyncKey(owner, key) {
|
|
818
839
|
if (this.reportedDeadKeys.has(key)) return;
|
|
840
|
+
if (this.aliveMs < DEAD_KEY_GRACE_MS) return;
|
|
819
841
|
this.reportedDeadKeys.add(key);
|
|
820
842
|
diagnose(owner.tree?.engine ?? null, "error", `[incanto] ${owner.getPath()}: sync key '${key}' reads as undefined — it is never sent, so remote players never see it change. Check the prop name and the node path.`);
|
|
821
843
|
}
|
|
822
844
|
sendAccumulator = 0;
|
|
845
|
+
/** Simulated ms since the last time the FULL sync set went out. */
|
|
846
|
+
keyframeAccumulator = 0;
|
|
847
|
+
/** Simulated ms this manager has been replicating — see reportDeadSyncKey. */
|
|
848
|
+
aliveMs = 0;
|
|
823
849
|
detachReplication = null;
|
|
824
850
|
lastOwner = null;
|
|
825
851
|
boundScene = null;
|
|
@@ -831,16 +857,17 @@ var NetworkManager = class NetworkManager {
|
|
|
831
857
|
const server = opts.transport ?? await createDefaultTransport(opts.config);
|
|
832
858
|
await server.connect();
|
|
833
859
|
const requested = opts.room ?? engine.scene?.multiplayer?.room ?? "auto";
|
|
834
|
-
const manager = new NetworkManager(engine, server, await server.remoteFunction("joinRoom", [requested === "auto" ? void 0 : requested], { needResponse: true }), opts.throttleMs ?? 50);
|
|
860
|
+
const manager = new NetworkManager(engine, server, await server.remoteFunction("joinRoom", [requested === "auto" ? void 0 : requested], { needResponse: true }), opts.throttleMs ?? 50, opts.keyframeMs ?? DEFAULT_KEYFRAME_MS);
|
|
835
861
|
managers.set(engine, manager);
|
|
836
862
|
return manager;
|
|
837
863
|
}
|
|
838
|
-
constructor(engine, server, roomId, throttleMs) {
|
|
864
|
+
constructor(engine, server, roomId, throttleMs, keyframeMs) {
|
|
839
865
|
this.engine = engine;
|
|
840
866
|
this.server = server;
|
|
841
867
|
this.roomId = roomId;
|
|
842
868
|
this.account = server.account;
|
|
843
869
|
this.throttleMs = Math.max(30, throttleMs);
|
|
870
|
+
this.keyframeMs = Math.max(0, keyframeMs);
|
|
844
871
|
this.boundScene = engine.scene;
|
|
845
872
|
this.subs.push(server.subscribeRoomState(roomId, (state) => {
|
|
846
873
|
this.latestRoomState = state;
|
|
@@ -996,6 +1023,7 @@ var NetworkManager = class NetworkManager {
|
|
|
996
1023
|
const scene = this.engine.scene;
|
|
997
1024
|
if (!scene || scene !== this.boundScene) return;
|
|
998
1025
|
this.sendAccumulator += dt * 1e3;
|
|
1026
|
+
this.aliveMs += dt * 1e3;
|
|
999
1027
|
const owner = findOwnerNode(scene.root, true);
|
|
1000
1028
|
if (!owner) return;
|
|
1001
1029
|
if (owner !== this.lastOwner) {
|
|
@@ -1005,22 +1033,29 @@ var NetworkManager = class NetworkManager {
|
|
|
1005
1033
|
const net = owner.network;
|
|
1006
1034
|
const window = Math.max(30, typeof net.throttleMs === "number" ? net.throttleMs : this.throttleMs);
|
|
1007
1035
|
const syncKeys = Array.isArray(net.sync) ? net.sync : [];
|
|
1008
|
-
const
|
|
1036
|
+
const full = {};
|
|
1037
|
+
let changed = false;
|
|
1009
1038
|
for (const key of syncKeys) {
|
|
1010
1039
|
const value = readSyncKey(owner, key);
|
|
1011
1040
|
if (value === void 0) {
|
|
1012
1041
|
this.reportDeadSyncKey(owner, key);
|
|
1013
1042
|
continue;
|
|
1014
1043
|
}
|
|
1015
|
-
|
|
1044
|
+
full[key] = jsonClone(value);
|
|
1045
|
+
if (!jsonEquals(this.lastSent.get(key) ?? null, value)) changed = true;
|
|
1016
1046
|
}
|
|
1047
|
+
this.keyframeAccumulator += dt * 1e3;
|
|
1048
|
+
const keyframeDue = this.keyframeMs > 0 && this.keyframeAccumulator >= this.keyframeMs;
|
|
1017
1049
|
if (this.sendAccumulator < window) return;
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1050
|
+
if (!changed && !keyframeDue) return;
|
|
1051
|
+
if (Object.keys(full).length === 0) return;
|
|
1052
|
+
for (const [key, value] of Object.entries(full)) this.lastSent.set(key, jsonClone(value));
|
|
1053
|
+
this.keyframeAccumulator = 0;
|
|
1020
1054
|
this.sendAccumulator = 0;
|
|
1021
|
-
this.setMyState({ sync:
|
|
1055
|
+
this.setMyState({ sync: full });
|
|
1022
1056
|
}
|
|
1023
1057
|
};
|
|
1058
|
+
/** The single `network.mode === 'owner'` node in a tree, or null. @internal */
|
|
1024
1059
|
function findOwnerNode(node, enforceSingle = false) {
|
|
1025
1060
|
const owners = [];
|
|
1026
1061
|
collectOwners(node, owners);
|
|
@@ -1032,6 +1067,13 @@ function collectOwners(node, out) {
|
|
|
1032
1067
|
for (const c of node.children) collectOwners(c, out);
|
|
1033
1068
|
}
|
|
1034
1069
|
/** `'position'` → own prop; `'Sprite.animation'` → child path + prop (last dot splits). */
|
|
1070
|
+
/**
|
|
1071
|
+
* Read one `network.sync` key off an owner node, exactly as the sender does.
|
|
1072
|
+
*
|
|
1073
|
+
* Exported so an agreement check reads GROUND TRUTH through the same last-dot
|
|
1074
|
+
* split the protocol uses — a second implementation would eventually disagree
|
|
1075
|
+
* with this one, and then the check would be measuring itself.
|
|
1076
|
+
*/
|
|
1035
1077
|
function readSyncKey(node, key) {
|
|
1036
1078
|
const lastDot = key.lastIndexOf(".");
|
|
1037
1079
|
const target = lastDot === -1 ? node : node.getNodeOrNull(key.slice(0, lastDot)) ?? void 0;
|
|
@@ -1176,6 +1218,7 @@ var NetworkSpawner = class extends Node {
|
|
|
1176
1218
|
}
|
|
1177
1219
|
}
|
|
1178
1220
|
};
|
|
1221
|
+
/** The child name a spawner gives an account's instance. @internal */
|
|
1179
1222
|
function sanitizeName(key) {
|
|
1180
1223
|
return key.replace(/[/%]/g, "_") || "remote";
|
|
1181
1224
|
}
|
|
@@ -1218,7 +1261,7 @@ async function createSplitScreen(opts) {
|
|
|
1218
1261
|
for (let i = 0; i < accounts.length; i++) {
|
|
1219
1262
|
const account = accounts[i];
|
|
1220
1263
|
const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + i } : {});
|
|
1221
|
-
engine.setScene(loadScene(jsonClone(opts.scene)));
|
|
1264
|
+
engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
|
|
1222
1265
|
const manager = await NetworkManager.create(engine, {
|
|
1223
1266
|
transport: server.createClient(account),
|
|
1224
1267
|
...opts.room !== void 0 || i > 0 ? { room: opts.room ?? players[0].manager.roomId } : {}
|
|
@@ -1275,4 +1318,4 @@ function hasBodyNamed(node, base) {
|
|
|
1275
1318
|
return false;
|
|
1276
1319
|
}
|
|
1277
1320
|
//#endregion
|
|
1278
|
-
export {
|
|
1321
|
+
export { NetworkManager as a, readSyncKey as c, createLocalGameServer as d, LoopbackHub as f, sanitizeName as i, LocalGameServer as l, registerNodesNet as n, applySyncPatch as o, LoopbackTransport as p, NetworkSpawner as r, findOwnerNode as s, createSplitScreen as t, LocalGameServerTransport as u };
|
|
@@ -214,6 +214,6 @@ function newUid() {
|
|
|
214
214
|
//#endregion
|
|
215
215
|
//#region src/index.ts
|
|
216
216
|
/** Engine version. Kept in sync with package.json by the release pipeline. */
|
|
217
|
-
const VERSION = "0.
|
|
217
|
+
const VERSION = "0.59.0";
|
|
218
218
|
//#endregion
|
|
219
219
|
export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as AudioPlayer } from "./register-
|
|
1
|
+
import { g as AudioPlayer } from "./register-CDrAQqPp.js";
|
|
2
2
|
import { a as logReport, s as parseDrive } from "./touch-DESwnpOc.js";
|
|
3
3
|
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
|
|
4
4
|
//#region src/core/audio-errors.ts
|