incanto 0.58.0 → 0.60.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 +45 -4
- 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-BCm38FJV.js} +6 -6
- package/dist/{create-game-BZwWJIns.js → create-game-ClnIb_M5.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-8cjF3t6w.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 +53 -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-DqdVp1bt.js} +12 -2
- package/dist/{physics-3d-Dnz4fsXX.js → physics-3d-BP0DZb_1.js} +13 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/{register-C6ZBFRjd.js → register-BSu2dWGC.js} +57 -27
- package/dist/{register-Ch70uByv.js → register-Da3hXh2H.js} +137 -46
- package/dist/{registry-C7u42TID.js → registry-WWcQcfMr.js} +1 -1
- package/dist/{replay-Dw6gMlYA.d.ts → replay-BCMK_VRP.d.ts} +15 -2
- package/dist/{replay-s7I2GstT.js → replay-BlNuIDdg.js} +41 -13
- package/dist/{split-screen-B0baBwxI.d.ts → split-screen-BQ3tAsf-.d.ts} +38 -1
- package/dist/{split-screen-DLsUrleX.js → split-screen-CL5Yvxse.js} +56 -13
- package/dist/{sprite-animation-C0wXLBZJ.js → sprite-animation-CY-mrr1L.js} +1 -1
- package/dist/{src-C1J09Op6.js → src-DFpXBMJN.js} +1 -1
- package/dist/{teardown-Cs113S9F.js → teardown-CTTwhWSe.js} +1 -1
- package/dist/{test-ZBga8kQ9.js → test-BeZ95pqw.js} +264 -25
- package/dist/test.d.ts +119 -5
- package/dist/test.js +3 -3
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-BQQjE9UQ.js → agent8-DSJries_.js} +1 -1
- package/editor/assets/{debug-C9UCsXBS.js → debug-D15Wi5TO.js} +1 -1
- package/editor/assets/{index-CBgfM3WD.js → index-BjC88k97.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 +64 -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,9 +1,9 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
1
|
+
import { C as Node, b as effectiveOrder, w as diagnose } from "./loader-D8n7TU8W.js";
|
|
2
|
+
import { I as translateOn, t as registerCoreNodes } from "./register-Da3hXh2H.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
|
-
import { i as getNodeSchema, l as registerNode } from "./registry-
|
|
6
|
-
import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-
|
|
5
|
+
import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
|
|
6
|
+
import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
|
|
7
7
|
import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
|
|
8
8
|
import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
|
|
9
9
|
//#region src/2d/physics/collider-validate.ts
|
|
@@ -115,32 +115,51 @@ var Node2D = class extends Node {
|
|
|
115
115
|
* `{shape:'capsule', radius, height}` — never child shape nodes.
|
|
116
116
|
*/
|
|
117
117
|
var PhysicsBody2D = class extends Node2D {
|
|
118
|
-
static props = {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
118
|
+
static props = {
|
|
119
|
+
collider: {
|
|
120
|
+
default: {},
|
|
121
|
+
variants: {
|
|
122
|
+
tag: "shape",
|
|
123
|
+
byTag: {
|
|
124
|
+
rect: {
|
|
125
|
+
shape: "rect",
|
|
126
|
+
size: [32, 32]
|
|
127
|
+
},
|
|
128
|
+
circle: {
|
|
129
|
+
shape: "circle",
|
|
130
|
+
radius: 16
|
|
131
|
+
},
|
|
132
|
+
capsule: {
|
|
133
|
+
shape: "capsule",
|
|
134
|
+
radius: 12,
|
|
135
|
+
height: 32
|
|
136
|
+
}
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Whether this body's collider takes part in physics right now.
|
|
142
|
+
*
|
|
143
|
+
* A melee hitbox is off most of the time and live for a few frames of a
|
|
144
|
+
* swing, and there was no way to say so: games expressed it by REPLACING
|
|
145
|
+
* `collider` with `{}`, which tears the rigid body down and builds a new
|
|
146
|
+
* one twice per swing, and makes the engine warn `has no collider —
|
|
147
|
+
* physics skips it` about a scene that is correct. Keep the real collider
|
|
148
|
+
* and turn it off.
|
|
149
|
+
*
|
|
150
|
+
* Off means no contacts and no `triggerEnter`/`triggerExit`. The body
|
|
151
|
+
* itself stays, so nothing is rebuilt and re-arming is one call.
|
|
152
|
+
*/
|
|
153
|
+
enabled: { default: true }
|
|
154
|
+
};
|
|
139
155
|
/** The unified collision model: every collider participant can emit these. */
|
|
140
156
|
static signals = ["triggerEnter", "triggerExit"];
|
|
141
157
|
_collider = {};
|
|
142
158
|
/** @internal Physics resync flag — raised when `collider` is REPLACED. */
|
|
143
159
|
_colliderChanged = false;
|
|
160
|
+
_enabled = true;
|
|
161
|
+
/** @internal Physics resync flag — raised when `enabled` is written. */
|
|
162
|
+
_enabledChanged = false;
|
|
144
163
|
/**
|
|
145
164
|
* Collider shape prop. Change it by REPLACING the object
|
|
146
165
|
* (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the
|
|
@@ -153,6 +172,15 @@ var PhysicsBody2D = class extends Node2D {
|
|
|
153
172
|
this._collider = value;
|
|
154
173
|
this._colliderChanged = true;
|
|
155
174
|
}
|
|
175
|
+
/** See the `enabled` prop. Writing it arms or disarms the existing collider. */
|
|
176
|
+
get enabled() {
|
|
177
|
+
return this._enabled;
|
|
178
|
+
}
|
|
179
|
+
set enabled(value) {
|
|
180
|
+
if (value === this._enabled) return;
|
|
181
|
+
this._enabled = value;
|
|
182
|
+
this._enabledChanged = true;
|
|
183
|
+
}
|
|
156
184
|
/** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
|
|
157
185
|
static validateJson(node) {
|
|
158
186
|
validateCollider2D(node.collider, node.name);
|
|
@@ -825,11 +853,12 @@ var Label = class extends Node2D {
|
|
|
825
853
|
_syncObject2D(assets) {
|
|
826
854
|
super._syncObject2D(assets);
|
|
827
855
|
const quad = this._quad();
|
|
828
|
-
|
|
856
|
+
const text = translateOn(this, this.text);
|
|
857
|
+
if (typeof document === "undefined" || text === "") {
|
|
829
858
|
quad.visible = false;
|
|
830
859
|
return;
|
|
831
860
|
}
|
|
832
|
-
const key = `${
|
|
861
|
+
const key = `${text}\0${this.fontSize}\0${this.color}\0${this.font}\0${this.align}`;
|
|
833
862
|
if (key !== this.lastKey) {
|
|
834
863
|
this.lastKey = key;
|
|
835
864
|
this.rasterize();
|
|
@@ -838,13 +867,14 @@ var Label = class extends Node2D {
|
|
|
838
867
|
quad.renderOrder = this.effectiveRenderOrder;
|
|
839
868
|
}
|
|
840
869
|
rasterize() {
|
|
870
|
+
const text = translateOn(this, this.text);
|
|
841
871
|
const dpr = Math.min(globalThis.devicePixelRatio ?? 1, 2);
|
|
842
872
|
this.canvas ??= document.createElement("canvas");
|
|
843
873
|
const ctx = this.canvas.getContext("2d");
|
|
844
874
|
if (!ctx) return;
|
|
845
875
|
const fontSpec = `${this.fontSize * dpr}px ${this.font}`;
|
|
846
876
|
ctx.font = fontSpec;
|
|
847
|
-
const metrics = ctx.measureText(
|
|
877
|
+
const metrics = ctx.measureText(text);
|
|
848
878
|
const w = Math.max(1, Math.ceil(metrics.width));
|
|
849
879
|
const h = Math.ceil(this.fontSize * dpr * 1.4);
|
|
850
880
|
this.canvas.width = w;
|
|
@@ -852,7 +882,7 @@ var Label = class extends Node2D {
|
|
|
852
882
|
ctx.font = fontSpec;
|
|
853
883
|
ctx.fillStyle = this.color;
|
|
854
884
|
ctx.textBaseline = "middle";
|
|
855
|
-
ctx.fillText(
|
|
885
|
+
ctx.fillText(text, 0, h / 2);
|
|
856
886
|
const quad = this._quad();
|
|
857
887
|
const mat = quad.material;
|
|
858
888
|
mat.map?.dispose();
|
|
@@ -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,45 @@ 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
|
+
/**
|
|
1588
|
+
* What this widget actually PAINTS, when that differs from its props.
|
|
1589
|
+
*
|
|
1590
|
+
* A headless capture printed `text="@t:menu.start"` — byte-identical in
|
|
1591
|
+
* every language — so a passing capture proved nothing about localization
|
|
1592
|
+
* and a broken translation was invisible to every check the engine has.
|
|
1593
|
+
* `incanto-frame` reads the WebGL buffer and cannot read DOM text at all.
|
|
1594
|
+
*
|
|
1595
|
+
* `undefined` when the painted words ARE the prop: recording it twice would
|
|
1596
|
+
* be noise on every widget in a game that never localizes anything.
|
|
1597
|
+
*/
|
|
1598
|
+
_paintedText() {}
|
|
1599
|
+
_labelText() {
|
|
1600
|
+
const label = this.label;
|
|
1601
|
+
return typeof label === "string" ? this._t(label) : "";
|
|
1602
|
+
}
|
|
1603
|
+
_tOr(key, fallback) {
|
|
1604
|
+
const locale = this.tree?.engine?.locale;
|
|
1605
|
+
return locale?.declares(key) ? locale.t(key) : fallback;
|
|
1606
|
+
}
|
|
1539
1607
|
};
|
|
1540
1608
|
/** A text line (score, timer, hints). Set `.text` from behaviors. */
|
|
1541
1609
|
var UiText = class extends HudWidgetBase {
|
|
@@ -1586,6 +1654,10 @@ var UiText = class extends HudWidgetBase {
|
|
|
1586
1654
|
if (this.format && this.slot !== null) return this._t(this.format).replace("{}", this.slot);
|
|
1587
1655
|
return this._t(this.text);
|
|
1588
1656
|
}
|
|
1657
|
+
_paintedText() {
|
|
1658
|
+
const painted = this.shown;
|
|
1659
|
+
return painted === this.text ? void 0 : painted;
|
|
1660
|
+
}
|
|
1589
1661
|
_build() {
|
|
1590
1662
|
const el = document.createElement("div");
|
|
1591
1663
|
el.style.cssText = this.look();
|
|
@@ -1756,6 +1828,7 @@ var UiBanner = class extends HudWidgetBase {
|
|
|
1756
1828
|
show(text, opts) {
|
|
1757
1829
|
this.queue.push({
|
|
1758
1830
|
text,
|
|
1831
|
+
source: text,
|
|
1759
1832
|
color: opts?.color ?? "#ffffff",
|
|
1760
1833
|
seconds: opts?.seconds ?? this.seconds
|
|
1761
1834
|
});
|
|
@@ -1788,6 +1861,11 @@ var UiBanner = class extends HudWidgetBase {
|
|
|
1788
1861
|
this._element.style.opacity = opacity;
|
|
1789
1862
|
}
|
|
1790
1863
|
if (this.current) {
|
|
1864
|
+
const next = this._t(this.current.source);
|
|
1865
|
+
if (next !== this.current.text) {
|
|
1866
|
+
this.current.text = next;
|
|
1867
|
+
if (this._element) this._element.textContent = next;
|
|
1868
|
+
}
|
|
1791
1869
|
if (this.current.seconds > 0) {
|
|
1792
1870
|
this.remaining -= dt;
|
|
1793
1871
|
if (this.remaining <= 0) {
|
|
@@ -1798,7 +1876,7 @@ var UiBanner = class extends HudWidgetBase {
|
|
|
1798
1876
|
}
|
|
1799
1877
|
if (!this.current && this.queue.length > 0) {
|
|
1800
1878
|
this.current = this.queue.shift();
|
|
1801
|
-
this.current.text = this._t(this.current.
|
|
1879
|
+
this.current.text = this._t(this.current.source);
|
|
1802
1880
|
this.remaining = this.current.seconds;
|
|
1803
1881
|
if (this._element) {
|
|
1804
1882
|
this._element.textContent = this.current.text;
|
|
@@ -2126,7 +2204,7 @@ var Settings = class {
|
|
|
2126
2204
|
* without knowing this class exists. Returns the unsubscribe.
|
|
2127
2205
|
*/
|
|
2128
2206
|
bindLocale(locale) {
|
|
2129
|
-
locale.locale = this.
|
|
2207
|
+
locale.locale = this.store.get("language", "") || suggestLocale(locale.available());
|
|
2130
2208
|
return locale.changed.connect((next) => {
|
|
2131
2209
|
this.set("language", next);
|
|
2132
2210
|
});
|
|
@@ -2229,6 +2307,7 @@ function qualityRendering(tier) {
|
|
|
2229
2307
|
}
|
|
2230
2308
|
//#endregion
|
|
2231
2309
|
//#region src/core/engine.ts
|
|
2310
|
+
var engine_exports = /* @__PURE__ */ __exportAll({ Engine: () => Engine });
|
|
2232
2311
|
const MAX_DT_SECONDS = .25;
|
|
2233
2312
|
function rafScheduler(cb) {
|
|
2234
2313
|
let live = true;
|
|
@@ -3050,10 +3129,14 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
3050
3129
|
}
|
|
3051
3130
|
/** Queue a line; optional `choices` renders buttons after the line types out. */
|
|
3052
3131
|
say(speaker, text, choices) {
|
|
3053
|
-
|
|
3132
|
+
const source = {
|
|
3054
3133
|
speaker,
|
|
3055
3134
|
text,
|
|
3056
3135
|
choices: choices ?? null
|
|
3136
|
+
};
|
|
3137
|
+
this.queue.push({
|
|
3138
|
+
...source,
|
|
3139
|
+
source
|
|
3057
3140
|
});
|
|
3058
3141
|
}
|
|
3059
3142
|
/**
|
|
@@ -3107,13 +3190,22 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
3107
3190
|
box.addEventListener("click", () => this.advance());
|
|
3108
3191
|
return box;
|
|
3109
3192
|
}
|
|
3193
|
+
/** The locale `current` was resolved in, so a switch is one compare away. */
|
|
3194
|
+
resolvedIn = "\0";
|
|
3195
|
+
/** Re-read the live line from its untouched source. */
|
|
3196
|
+
resolveCurrent() {
|
|
3197
|
+
const line = this.current;
|
|
3198
|
+
if (!line) return;
|
|
3199
|
+
line.text = this._t(line.source.text);
|
|
3200
|
+
line.speaker = this._t(line.source.speaker);
|
|
3201
|
+
line.choices = line.source.choices ? line.source.choices.map((c) => this._t(c)) : null;
|
|
3202
|
+
this.resolvedIn = this.tree?.engine?.locale.locale ?? "";
|
|
3203
|
+
}
|
|
3110
3204
|
update(dt) {
|
|
3111
3205
|
super.update(dt);
|
|
3112
3206
|
if (!this.current && this.queue.length > 0) {
|
|
3113
3207
|
this.current = this.queue.shift();
|
|
3114
|
-
this.
|
|
3115
|
-
this.current.speaker = this._t(this.current.speaker);
|
|
3116
|
-
if (this.current.choices) this.current.choices = this.current.choices.map((c) => this._t(c));
|
|
3208
|
+
this.resolveCurrent();
|
|
3117
3209
|
this.revealed = this.charsPerSecond <= 0 ? this.current.text.length : 0;
|
|
3118
3210
|
this.emit("lineShown", this.current.text);
|
|
3119
3211
|
if (this._element) {
|
|
@@ -3123,6 +3215,13 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
3123
3215
|
}
|
|
3124
3216
|
if (this._element) this._element.style.display = this.visible && this.current ? "block" : "none";
|
|
3125
3217
|
if (!this.current) return;
|
|
3218
|
+
if ((this.tree?.engine?.locale.locale ?? "") !== this.resolvedIn) {
|
|
3219
|
+
const before = this.current.text.length;
|
|
3220
|
+
const progress = before > 0 ? this.revealed / before : 1;
|
|
3221
|
+
this.resolveCurrent();
|
|
3222
|
+
this.revealed = Math.round(progress * this.current.text.length);
|
|
3223
|
+
if (this.speakerEl) this.speakerEl.textContent = this.current.speaker;
|
|
3224
|
+
}
|
|
3126
3225
|
const wasTyping = this.revealed < this.current.text.length;
|
|
3127
3226
|
if (wasTyping) this.revealed = Math.min(this.current.text.length, this.revealed + this.charsPerSecond * dt);
|
|
3128
3227
|
const doneTyping = this.revealed >= this.current.text.length;
|
|
@@ -3193,17 +3292,6 @@ function px(n) {
|
|
|
3193
3292
|
return `${n}px`;
|
|
3194
3293
|
}
|
|
3195
3294
|
/**
|
|
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
3295
|
* A box that holds other widgets — the thing a menu actually is.
|
|
3208
3296
|
*
|
|
3209
3297
|
* Widgets normally mount into their HudLayer's anchor slot, which is why a
|
|
@@ -3377,7 +3465,7 @@ var UiSlider = class extends HudWidgetBase {
|
|
|
3377
3465
|
const wrap = document.createElement("div");
|
|
3378
3466
|
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
3467
|
const text = document.createElement("span");
|
|
3380
|
-
text.textContent = this.
|
|
3468
|
+
text.textContent = this._labelText();
|
|
3381
3469
|
const input = document.createElement("input");
|
|
3382
3470
|
input.type = "range";
|
|
3383
3471
|
input.min = String(this.min);
|
|
@@ -3390,7 +3478,7 @@ var UiSlider = class extends HudWidgetBase {
|
|
|
3390
3478
|
this.last = this.value;
|
|
3391
3479
|
this.emit("changed", this.value);
|
|
3392
3480
|
});
|
|
3393
|
-
if (this.
|
|
3481
|
+
if (this._labelText()) wrap.appendChild(text);
|
|
3394
3482
|
wrap.appendChild(input);
|
|
3395
3483
|
this.input = input;
|
|
3396
3484
|
this.text = text;
|
|
@@ -3402,7 +3490,7 @@ var UiSlider = class extends HudWidgetBase {
|
|
|
3402
3490
|
this.last = this.value;
|
|
3403
3491
|
this.input.value = String(this.value);
|
|
3404
3492
|
}
|
|
3405
|
-
const label = this.
|
|
3493
|
+
const label = this._labelText();
|
|
3406
3494
|
if (this.text && this.text.textContent !== label) this.text.textContent = label;
|
|
3407
3495
|
}
|
|
3408
3496
|
};
|
|
@@ -3442,9 +3530,9 @@ var UiToggle = class extends HudWidgetBase {
|
|
|
3442
3530
|
this.emit("changed", this.value);
|
|
3443
3531
|
});
|
|
3444
3532
|
const text = document.createElement("span");
|
|
3445
|
-
text.textContent = this.
|
|
3533
|
+
text.textContent = this._labelText();
|
|
3446
3534
|
this.tag = text;
|
|
3447
|
-
this.lastLabel = this.
|
|
3535
|
+
this.lastLabel = this._labelText();
|
|
3448
3536
|
wrap.append(input, text);
|
|
3449
3537
|
this.input = input;
|
|
3450
3538
|
this.last = this.value;
|
|
@@ -3457,7 +3545,7 @@ var UiToggle = class extends HudWidgetBase {
|
|
|
3457
3545
|
this.last = this.value;
|
|
3458
3546
|
this.input.checked = this.value;
|
|
3459
3547
|
}
|
|
3460
|
-
const label = this.
|
|
3548
|
+
const label = this._labelText();
|
|
3461
3549
|
if (this.tag && label !== this.lastLabel) {
|
|
3462
3550
|
this.lastLabel = label;
|
|
3463
3551
|
this.tag.textContent = label;
|
|
@@ -3507,24 +3595,33 @@ var UiSelect = class extends HudWidgetBase {
|
|
|
3507
3595
|
this.last = this.value;
|
|
3508
3596
|
this.emit("changed", this.value);
|
|
3509
3597
|
});
|
|
3510
|
-
if (this.
|
|
3598
|
+
if (this._labelText()) wrap.appendChild(text);
|
|
3511
3599
|
wrap.appendChild(select);
|
|
3512
3600
|
this.select = select;
|
|
3513
3601
|
this.fillOptions();
|
|
3514
3602
|
return wrap;
|
|
3515
3603
|
}
|
|
3516
|
-
/**
|
|
3604
|
+
/**
|
|
3605
|
+
* What an option READS as, when it differs from its value.
|
|
3606
|
+
*
|
|
3607
|
+
* The base resolves `@t:` like every other text prop: the VALUE stays exactly
|
|
3608
|
+
* what the author wrote — so a `changed` handler never has to know the active
|
|
3609
|
+
* language — and only the displayed words change. Without this a dropdown was
|
|
3610
|
+
* the one widget with no way to speak anything but English, and
|
|
3611
|
+
* `"options": "@t:diff.easy"` rendered that marker literally on screen.
|
|
3612
|
+
*/
|
|
3517
3613
|
_optionLabel(value) {
|
|
3518
|
-
return value;
|
|
3614
|
+
return this._t(value);
|
|
3519
3615
|
}
|
|
3520
3616
|
fillOptions() {
|
|
3521
3617
|
const select = this.select;
|
|
3522
|
-
if (!select
|
|
3523
|
-
|
|
3618
|
+
if (!select) return;
|
|
3619
|
+
const values = this.options.split(",").map((o) => o.trim()).filter(Boolean);
|
|
3620
|
+
const painted = values.map((v) => `${v}\u0000${this._optionLabel(v)}`).join("");
|
|
3621
|
+
if (painted === this.lastOptions) return;
|
|
3622
|
+
this.lastOptions = painted;
|
|
3524
3623
|
select.textContent = "";
|
|
3525
|
-
for (const
|
|
3526
|
-
const value = raw.trim();
|
|
3527
|
-
if (!value) continue;
|
|
3624
|
+
for (const value of values) {
|
|
3528
3625
|
const option = document.createElement("option");
|
|
3529
3626
|
option.value = value;
|
|
3530
3627
|
option.textContent = this._optionLabel(value);
|
|
@@ -3635,9 +3732,6 @@ var UiSettingSelect = class extends UiSelect {
|
|
|
3635
3732
|
* `settings.quality.high` is worse than one reading `High`. English is the
|
|
3636
3733
|
* base language and the fallback, always.
|
|
3637
3734
|
*/
|
|
3638
|
-
_tOr(key, fallback) {
|
|
3639
|
-
return translated(this._engine(), key, fallback);
|
|
3640
|
-
}
|
|
3641
3735
|
_sync() {
|
|
3642
3736
|
const engine = this._engine();
|
|
3643
3737
|
if (engine) {
|
|
@@ -3807,12 +3901,10 @@ var UiVolumeSlider = class extends UiSlider {
|
|
|
3807
3901
|
bus = "master";
|
|
3808
3902
|
/** Last value we PUSHED in, to tell an engine-side change from a player drag. */
|
|
3809
3903
|
lastPushed = null;
|
|
3810
|
-
|
|
3811
|
-
if (
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
}
|
|
3815
|
-
return super._build();
|
|
3904
|
+
_labelText() {
|
|
3905
|
+
if (this.label) return this._t(this.label);
|
|
3906
|
+
const english = VOLUME_LABELS[this.bus] ?? VOLUME_LABELS.master;
|
|
3907
|
+
return this._tOr(`settings.volume.${this.bus}`, english);
|
|
3816
3908
|
}
|
|
3817
3909
|
_sync() {
|
|
3818
3910
|
const audio = this.tree?.engine?.audio;
|
|
@@ -3846,9 +3938,8 @@ var UiMuteToggle = class extends UiToggle {
|
|
|
3846
3938
|
label: { default: "" }
|
|
3847
3939
|
};
|
|
3848
3940
|
lastPushed = null;
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
return super._build();
|
|
3941
|
+
_labelText() {
|
|
3942
|
+
return this.label ? this._t(this.label) : this._tOr("settings.mute", "Mute");
|
|
3852
3943
|
}
|
|
3853
3944
|
_sync() {
|
|
3854
3945
|
const audio = this.tree?.engine?.audio;
|
|
@@ -3894,4 +3985,4 @@ function registerCoreNodes() {
|
|
|
3894
3985
|
registerNode(UiMuteToggle);
|
|
3895
3986
|
}
|
|
3896
3987
|
//#endregion
|
|
3897
|
-
export {
|
|
3988
|
+
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,9 +1,22 @@
|
|
|
1
|
-
import { b as Engine } from "./behavior-
|
|
1
|
+
import { b as Engine } from "./behavior-DWKTUzKI.js";
|
|
2
2
|
import { s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/audit.d.ts
|
|
5
5
|
/** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
|
|
6
|
-
|
|
6
|
+
interface AuditOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Every locale table the whole PROJECT declares, unioned.
|
|
9
|
+
*
|
|
10
|
+
* Localization tables MERGE across scenes — a game's shared UI strings live
|
|
11
|
+
* in the scene it boots from, and a level that adds three lines of its own
|
|
12
|
+
* does not wipe them. Auditing one file in isolation therefore reported a key
|
|
13
|
+
* declared in the boot scene as "nothing declares" it, and told its author to
|
|
14
|
+
* add a string they already have. The checker passes the corpus in when it
|
|
15
|
+
* has one.
|
|
16
|
+
*/
|
|
17
|
+
declaredElsewhere?: Record<string, Record<string, string>>;
|
|
18
|
+
}
|
|
19
|
+
declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
|
|
7
20
|
//#endregion
|
|
8
21
|
//#region src/core/errors.d.ts
|
|
9
22
|
/**
|
|
@@ -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-Da3hXh2H.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.
|
|
@@ -266,8 +266,7 @@ function isLit(environment) {
|
|
|
266
266
|
const ambient = environment.ambient;
|
|
267
267
|
return typeof ambient?.intensity === "number" && ambient.intensity > 0;
|
|
268
268
|
}
|
|
269
|
-
|
|
270
|
-
function auditScene(scene) {
|
|
269
|
+
function auditScene(scene, opts = {}) {
|
|
271
270
|
const warnings = [];
|
|
272
271
|
const root = scene.root;
|
|
273
272
|
if (!root) return warnings;
|
|
@@ -301,7 +300,7 @@ function auditScene(scene) {
|
|
|
301
300
|
if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
|
|
302
301
|
if (scene.dimension === "3d" && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
|
|
303
302
|
for (const warning of nodeRefWarnings(scene)) warnings.push(warning);
|
|
304
|
-
for (const warning of auditStrings(scene)) warnings.push(warning);
|
|
303
|
+
for (const warning of auditStrings(scene, opts.declaredElsewhere)) warnings.push(warning);
|
|
305
304
|
return warnings;
|
|
306
305
|
}
|
|
307
306
|
/**
|
|
@@ -317,15 +316,33 @@ function auditScene(scene) {
|
|
|
317
316
|
* screen. Reported here — at author time, where it can still be fixed — rather
|
|
318
317
|
* than at runtime in front of someone playing.
|
|
319
318
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Close to `@t:` without being it — `@T:`, a leading space, `@t :`, `@t.`.
|
|
321
|
+
* Deliberately narrow: an at-sign in ordinary prose ("email me @ home") must
|
|
322
|
+
* not trip it.
|
|
323
|
+
*/
|
|
324
|
+
const NEAR_MISS = /^\s*@\s*t\s*[:.]/i;
|
|
325
|
+
function auditStrings(scene, elsewhere) {
|
|
326
|
+
const own = scene.strings;
|
|
327
|
+
const tables = {};
|
|
328
|
+
for (const source of [elsewhere, own]) for (const [locale, entries] of Object.entries(source ?? {})) tables[locale] = {
|
|
329
|
+
...tables[locale] ?? {},
|
|
330
|
+
...entries
|
|
331
|
+
};
|
|
322
332
|
const declared = /* @__PURE__ */ new Set();
|
|
323
|
-
for (const entries of Object.values(tables
|
|
333
|
+
for (const entries of Object.values(tables)) for (const key of Object.keys(entries)) declared.add(key);
|
|
334
|
+
const inBase = new Set(Object.keys(tables["en"] ?? {}));
|
|
324
335
|
const missing = /* @__PURE__ */ new Set();
|
|
336
|
+
const notInEnglish = /* @__PURE__ */ new Set();
|
|
337
|
+
const nearMiss = /* @__PURE__ */ new Set();
|
|
325
338
|
const walk = (value) => {
|
|
326
339
|
if (typeof value === "string") {
|
|
340
|
+
if (translationKey(value) === null && NEAR_MISS.test(value)) nearMiss.add(value);
|
|
327
341
|
const key = translationKey(value);
|
|
328
|
-
if (key !== null
|
|
342
|
+
if (key !== null) {
|
|
343
|
+
if (!declared.has(key)) missing.add(key);
|
|
344
|
+
else if (declared.size > 0 && !inBase.has(key)) notInEnglish.add(key);
|
|
345
|
+
}
|
|
329
346
|
return;
|
|
330
347
|
}
|
|
331
348
|
if (Array.isArray(value)) {
|
|
@@ -335,9 +352,20 @@ function auditStrings(scene) {
|
|
|
335
352
|
if (value !== null && typeof value === "object") for (const item of Object.values(value)) walk(item);
|
|
336
353
|
};
|
|
337
354
|
walk(scene.root);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
355
|
+
const out = [];
|
|
356
|
+
if (missing.size > 0) {
|
|
357
|
+
const listed = [...missing].sort();
|
|
358
|
+
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.`);
|
|
359
|
+
}
|
|
360
|
+
if (nearMiss.size > 0) {
|
|
361
|
+
const listed = [...nearMiss].sort();
|
|
362
|
+
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.`);
|
|
363
|
+
}
|
|
364
|
+
if (notInEnglish.size > 0) {
|
|
365
|
+
const listed = [...notInEnglish].sort();
|
|
366
|
+
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.`);
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
341
369
|
}
|
|
342
370
|
//#endregion
|
|
343
371
|
//#region src/core/replay.ts
|