incanto 0.34.0 → 0.35.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 +5 -5
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +122 -14
- package/dist/3d.js +5 -5
- package/dist/{audit-C4kmDK0o.js → audit-C5oZGAru.js} +37 -0
- package/dist/{behavior-CtPScDMo.d.ts → behavior-CGSCWOHB.d.ts} +310 -22
- package/dist/{create-game-CFNqoqOX.js → create-game-B0Wfhi2-.js} +6 -5
- package/dist/{create-game-QTfghzwM.js → create-game-BUD89Kqh.js} +20 -6
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-DlOvknDO.js → duplicate-C716f-97.js} +1 -1
- package/dist/{editor-switch-BJb-CWfA.d.ts → editor-switch-DyXEtH36.d.ts} +1 -1
- package/dist/editor.js +1739 -1312
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-B6WiUsaO.js → environment-presets-TAGvmM3z.js} +240 -74
- package/dist/{errors-1dXlIwoR.d.ts → errors-BY2kL0hv.d.ts} +1 -1
- package/dist/{gameplay-BxFtmfSe.js → gameplay-BvhcQbfJ.js} +75 -11
- package/dist/gameplay.d.ts +12 -5
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +22 -7
- package/dist/index.js +6 -6
- package/dist/{loader-DmXfj6Uv.d.ts → loader-CUcj00M8.d.ts} +19 -2
- package/dist/{loader-BwR0DxeM.js → loader-Mig5fY4n.js} +97 -18
- package/dist/net.d.ts +2 -2
- package/dist/net.js +3 -3
- package/dist/{particle-sim-CwJ5rI_P.d.ts → particle-sim-B-vZBF5R.d.ts} +1 -1
- package/dist/{pathfinding-DRCe89SI.d.ts → pathfinding-DgOo2KNF.d.ts} +1 -1
- package/dist/{physics-2d-BuyAwsW5.js → physics-2d-DqAclql-.js} +22 -9
- package/dist/{physics-3d-C8Kn_Nyb.js → physics-3d-DxBH4sIF.js} +27 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-G3edsjJ2.js → register-BFLg0-_i.js} +490 -12
- package/dist/{register-OodmuUMK.js → register-BjbPMA5B.js} +2 -2
- package/dist/{register-BLPC10Mj.js → register-DSmIRAf7.js} +11 -5
- package/dist/{schema-CcoWb32N.d.ts → schema-3ywbdlrv.d.ts} +10 -0
- package/dist/{test-C4B5znap.js → test-9EokzbRd.js} +10 -10
- package/dist/test.d.ts +4 -4
- package/dist/test.js +2 -2
- package/dist/vite.js +1 -1
- package/editor/assets/{agent8-BTODHtdM.js → agent8-D3_GWeuh.js} +1 -1
- package/editor/assets/{debug-CJEz4EwC.js → debug-CX0LDd1r.js} +1 -1
- package/editor/assets/index-CMlKFT0C.js +10773 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +109 -0
- package/skills/incanto-assets.md +21 -0
- package/skills/incanto-behaviors-and-scripts.md +16 -0
- package/skills/incanto-editor.md +17 -1
- package/skills/incanto-gameplay-behaviors.md +6 -0
- package/skills/incanto-hud.md +16 -0
- package/skills/incanto-localization.md +132 -0
- package/skills/incanto-node-reference.md +33 -18
- package/skills/incanto-save-slots.md +152 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/PROJECT/Context.md +16 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/village.scene.json +25 -3
- package/editor/assets/index-BYCso8Bf.js +0 -10696
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as Node, h as
|
|
1
|
+
import { C as Signal, _ as behaviorSchema, f as Node, h as diagnose, m as InputMap } from "./loader-Mig5fY4n.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-CJdGpT2V.js";
|
|
@@ -924,6 +924,120 @@ var SfxEngine = class {
|
|
|
924
924
|
}
|
|
925
925
|
};
|
|
926
926
|
//#endregion
|
|
927
|
+
//#region src/core/localization.ts
|
|
928
|
+
/** The base locale. Every other locale falls back to it, one key at a time. */
|
|
929
|
+
const BASE_LOCALE = "en";
|
|
930
|
+
/**
|
|
931
|
+
* The marker that makes a string prop translatable: `"@t:hud.score"`.
|
|
932
|
+
*
|
|
933
|
+
* A prefix rather than an object wrapper, matching the `"$assetKey"` reference
|
|
934
|
+
* already in the engine — the prop is declared a string, so its value should
|
|
935
|
+
* stay a string. It also stays authorable by hand:
|
|
936
|
+
*
|
|
937
|
+
* ```json
|
|
938
|
+
* { "type": "UiText", "props": { "text": "@t:hud.score" } }
|
|
939
|
+
* ```
|
|
940
|
+
*/
|
|
941
|
+
const T_PREFIX = "@t:";
|
|
942
|
+
/** The key a `"@t:…"` value names, or null for an ordinary string. */
|
|
943
|
+
function translationKey(value) {
|
|
944
|
+
return value.startsWith("@t:") ? value.slice(3) : null;
|
|
945
|
+
}
|
|
946
|
+
/** `Hello, {name}` + `{name: 'Ada'}` → `Hello, Ada`. Unknown slots stay put. */
|
|
947
|
+
function interpolate(text, params) {
|
|
948
|
+
if (!params) return text;
|
|
949
|
+
return text.replace(/\{(\w+)\}/g, (whole, name) => name in params ? String(params[name]) : whole);
|
|
950
|
+
}
|
|
951
|
+
var Localization = class {
|
|
952
|
+
/** Fires with the new locale after it changes — for anything that caches. */
|
|
953
|
+
changed = new Signal();
|
|
954
|
+
tables = {};
|
|
955
|
+
current = "en";
|
|
956
|
+
constructor(tables, locale) {
|
|
957
|
+
if (tables) this.load(tables);
|
|
958
|
+
if (locale) this.locale = locale;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Merge a scene's `strings` table in.
|
|
962
|
+
*
|
|
963
|
+
* Merged rather than replaced: a game's shared UI strings live in the scene it
|
|
964
|
+
* boots from, and a level that adds three lines of its own should not wipe
|
|
965
|
+
* them. Per-key, so a level CAN override one string.
|
|
966
|
+
*/
|
|
967
|
+
load(tables) {
|
|
968
|
+
for (const [locale, entries] of Object.entries(tables)) {
|
|
969
|
+
this.tables[locale] ??= {};
|
|
970
|
+
const target = this.tables[locale];
|
|
971
|
+
for (const [key, text] of Object.entries(entries)) if (typeof text === "string") target[key] = text;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
get locale() {
|
|
975
|
+
return this.current;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Switch language. Setting one nobody declared is not an error — it simply
|
|
979
|
+
* resolves everything through the base locale, which is the correct outcome
|
|
980
|
+
* for a player whose browser reports a language this game does not ship.
|
|
981
|
+
*/
|
|
982
|
+
set locale(next) {
|
|
983
|
+
if (next === this.current || next === "") return;
|
|
984
|
+
this.current = next;
|
|
985
|
+
this.changed.emit(next);
|
|
986
|
+
}
|
|
987
|
+
/** Every locale with at least one string — what a language picker offers. */
|
|
988
|
+
available() {
|
|
989
|
+
const out = new Set(["en"]);
|
|
990
|
+
for (const [locale, entries] of Object.entries(this.tables)) if (Object.keys(entries).length > 0) out.add(locale);
|
|
991
|
+
return [...out];
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* The string for `key`: current locale, else English, else the key itself.
|
|
995
|
+
*
|
|
996
|
+
* Returning the key rather than an empty string is deliberate — a screen
|
|
997
|
+
* reading `menu.credits` tells an author exactly what to add, where a blank
|
|
998
|
+
* one tells them nothing and looks like a rendering bug.
|
|
999
|
+
*/
|
|
1000
|
+
t(key, params) {
|
|
1001
|
+
const own = this.tables[this.current]?.[key];
|
|
1002
|
+
if (own !== void 0) return interpolate(own, params);
|
|
1003
|
+
const base = this.tables["en"]?.[key];
|
|
1004
|
+
if (base !== void 0) return interpolate(base, params);
|
|
1005
|
+
return key;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Resolve a prop value: `"@t:key"` translates, anything else passes through.
|
|
1009
|
+
*
|
|
1010
|
+
* This is what every text-bearing widget calls, every frame. A plain string is
|
|
1011
|
+
* returned untouched, so a game that never localizes anything pays a
|
|
1012
|
+
* `startsWith` per widget per frame and nothing else.
|
|
1013
|
+
*/
|
|
1014
|
+
resolve(value, params) {
|
|
1015
|
+
const key = translationKey(value);
|
|
1016
|
+
return key === null ? value : this.t(key, params);
|
|
1017
|
+
}
|
|
1018
|
+
/** Whether any locale declares this key — for the audit, not for the runtime. */
|
|
1019
|
+
has(key) {
|
|
1020
|
+
for (const entries of Object.values(this.tables)) if (entries[key] !== void 0) return true;
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
/**
|
|
1025
|
+
* The player's language, if the browser will say and the game ships it.
|
|
1026
|
+
*
|
|
1027
|
+
* A starting point only — the language setting is the real answer, exactly as
|
|
1028
|
+
* `suggestQuality` is a starting point for the quality tier.
|
|
1029
|
+
*/
|
|
1030
|
+
function suggestLocale(available, languages) {
|
|
1031
|
+
const nav = globalThis.navigator;
|
|
1032
|
+
const wanted = languages ?? nav?.languages ?? [];
|
|
1033
|
+
for (const tag of wanted) {
|
|
1034
|
+
const base = tag.split("-")[0] ?? "";
|
|
1035
|
+
const hit = available.find((a) => a === tag || a === base);
|
|
1036
|
+
if (hit) return hit;
|
|
1037
|
+
}
|
|
1038
|
+
return "en";
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
927
1041
|
//#region src/core/frame-stats.ts
|
|
928
1042
|
/**
|
|
929
1043
|
* Rolling window of REAL frame timestamps — the math behind `engine.stats()`
|
|
@@ -1071,6 +1185,214 @@ function storageOrNull() {
|
|
|
1071
1185
|
}
|
|
1072
1186
|
}
|
|
1073
1187
|
//#endregion
|
|
1188
|
+
//#region src/core/save-slots.ts
|
|
1189
|
+
/**
|
|
1190
|
+
* Save slots — "continue where you left off", which no game on this engine could
|
|
1191
|
+
* do.
|
|
1192
|
+
*
|
|
1193
|
+
* `createSaveStore` has been here a long time and `Scene.toJSON()` round-trips a
|
|
1194
|
+
* live tree, so two thirds of the work existed. The missing third was that
|
|
1195
|
+
* BEHAVIOR state had nowhere to go: `Behavior` declared `onEnterTree`, `onReady`,
|
|
1196
|
+
* `onExitTree`, `update` and `fixedUpdate` and nothing for "write yourself down",
|
|
1197
|
+
* so `Health.current`, `ScoreKeeper.score` and every quest flag were
|
|
1198
|
+
* unrecoverable. Both quest templates this repo ships were RPGs you could not
|
|
1199
|
+
* resume.
|
|
1200
|
+
*
|
|
1201
|
+
* ## What a save is, and what it deliberately is NOT
|
|
1202
|
+
*
|
|
1203
|
+
* A save is **which scene, plus every behavior's state, keyed by node uid**. It
|
|
1204
|
+
* is not a snapshot of the live tree.
|
|
1205
|
+
*
|
|
1206
|
+
* That is a design decision, not a shortcut. Restoring a live tree by walking
|
|
1207
|
+
* paths breaks on this engine specifically:
|
|
1208
|
+
*
|
|
1209
|
+
* - `Spawner.onReady` DETACHES its prefab template from the tree (see
|
|
1210
|
+
* `SpawnSource.resolveTemplate`), so a captured tree and a freshly booted one
|
|
1211
|
+
* legitimately disagree about which nodes exist, every time.
|
|
1212
|
+
* Every scene with a spawner would report phantom removals.
|
|
1213
|
+
* - A spawned enemy carrying its own spawner has already lost ITS template, so
|
|
1214
|
+
* re-adding that subtree runs `onReady` against a node that is not there and
|
|
1215
|
+
* throws — and `onReady` is not quarantined, so it kills the whole load.
|
|
1216
|
+
* - Prop deltas cannot be applied with `applySchemaProps`, which resets every
|
|
1217
|
+
* prop to its default before writing.
|
|
1218
|
+
*
|
|
1219
|
+
* Reloading the scene from source sidesteps all three: the structure comes from
|
|
1220
|
+
* the file (which is authoritative and already validated), and the save carries
|
|
1221
|
+
* only what the file cannot know. A fresh boot detaches the same templates the
|
|
1222
|
+
* captured one did, so there is nothing to reconcile.
|
|
1223
|
+
*
|
|
1224
|
+
* The cost, stated plainly so nobody discovers it: **spawned enemies and
|
|
1225
|
+
* mid-level positions are not restored.** You resume at the scene's start with
|
|
1226
|
+
* your stats, inventory, unlocks and quest flags intact. That is what a
|
|
1227
|
+
* checkpoint save is, and it is what both quest templates need. A game that
|
|
1228
|
+
* wants a position saves it — `serialize()` can return anything.
|
|
1229
|
+
*
|
|
1230
|
+
* Node uids are the join key because they are the one identifier the engine
|
|
1231
|
+
* already promises survives a rename or a reparent.
|
|
1232
|
+
*/
|
|
1233
|
+
/**
|
|
1234
|
+
* Every behavior in the tree that has something to say, by uid.
|
|
1235
|
+
*
|
|
1236
|
+
* A behavior with no `serialize` contributes nothing — the hook is optional in
|
|
1237
|
+
* exactly the way the other five are, so adding save support to a game is
|
|
1238
|
+
* additive and a game that never saves pays nothing.
|
|
1239
|
+
*
|
|
1240
|
+
* A node with no `uid` is skipped and reported, because state keyed by nothing
|
|
1241
|
+
* cannot be given back. The editor assigns a uid to every node it touches; a
|
|
1242
|
+
* hand-written scene may not have.
|
|
1243
|
+
*/
|
|
1244
|
+
function captureBehaviors(root) {
|
|
1245
|
+
const state = {};
|
|
1246
|
+
const unaddressable = [];
|
|
1247
|
+
const walk = (node) => {
|
|
1248
|
+
const behavior = node.behavior;
|
|
1249
|
+
if (behavior && typeof behavior.serialize === "function") {
|
|
1250
|
+
const value = behavior.serialize();
|
|
1251
|
+
if (value !== void 0) if (node.uid) state[node.uid] = value;
|
|
1252
|
+
else unaddressable.push(node.getPath());
|
|
1253
|
+
}
|
|
1254
|
+
for (const child of node.children) walk(child);
|
|
1255
|
+
};
|
|
1256
|
+
walk(root);
|
|
1257
|
+
return {
|
|
1258
|
+
state,
|
|
1259
|
+
unaddressable
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Hand each behavior its state back.
|
|
1264
|
+
*
|
|
1265
|
+
* Call this AFTER the scene has loaded and `onReady` has run: `onReady` is where
|
|
1266
|
+
* a behavior sets its starting values, so restoring first would be overwritten
|
|
1267
|
+
* by the fresh start.
|
|
1268
|
+
*
|
|
1269
|
+
* Never throws on a mismatch. A save from an older build of the game will name
|
|
1270
|
+
* uids that no longer exist, and refusing to load it would mean a patch that
|
|
1271
|
+
* moves one node deletes everyone's progress. It reports instead.
|
|
1272
|
+
*/
|
|
1273
|
+
function restoreBehaviors(root, state) {
|
|
1274
|
+
const report = {
|
|
1275
|
+
missing: [],
|
|
1276
|
+
skipped: [],
|
|
1277
|
+
restored: 0
|
|
1278
|
+
};
|
|
1279
|
+
for (const [uid, value] of Object.entries(state)) {
|
|
1280
|
+
const node = root.getNodeByUid(uid);
|
|
1281
|
+
if (!node) {
|
|
1282
|
+
report.missing.push(uid);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const behavior = node.behavior;
|
|
1286
|
+
if (!behavior || typeof behavior.deserialize !== "function") {
|
|
1287
|
+
report.skipped.push(uid);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
behavior.deserialize(value);
|
|
1292
|
+
report.restored += 1;
|
|
1293
|
+
} catch {
|
|
1294
|
+
report.skipped.push(uid);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return report;
|
|
1298
|
+
}
|
|
1299
|
+
/** Slot ids, so a "3 save slots" menu is a list and not a naming convention. */
|
|
1300
|
+
const INDEX_KEY = "slots";
|
|
1301
|
+
/**
|
|
1302
|
+
* The slot layer: a named list of saves over the existing store.
|
|
1303
|
+
*
|
|
1304
|
+
* Slot ids are yours (`'auto'`, `'1'`, `'2'`, `'3'`) — the engine does not
|
|
1305
|
+
* decide how many you have or whether one of them autosaves.
|
|
1306
|
+
*/
|
|
1307
|
+
var SaveSlots = class {
|
|
1308
|
+
store;
|
|
1309
|
+
constructor(namespace = "saves", store) {
|
|
1310
|
+
this.store = store ?? createSaveStore(namespace);
|
|
1311
|
+
}
|
|
1312
|
+
/** Slot ids that have a save, newest first. */
|
|
1313
|
+
list() {
|
|
1314
|
+
const rows = this.store.get(INDEX_KEY, []).map((id) => ({
|
|
1315
|
+
id,
|
|
1316
|
+
slot: this.read(id)
|
|
1317
|
+
})).filter((row) => row.slot !== null);
|
|
1318
|
+
rows.sort((a, b) => b.slot.savedAt - a.slot.savedAt);
|
|
1319
|
+
return rows.map((row) => row.id);
|
|
1320
|
+
}
|
|
1321
|
+
/** Every slot with its contents — what a load menu actually renders. */
|
|
1322
|
+
all() {
|
|
1323
|
+
return this.list().map((id) => ({
|
|
1324
|
+
id,
|
|
1325
|
+
...this.read(id)
|
|
1326
|
+
}));
|
|
1327
|
+
}
|
|
1328
|
+
read(id) {
|
|
1329
|
+
const raw = this.store.get(key(id), null);
|
|
1330
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1331
|
+
const slot = raw;
|
|
1332
|
+
if (typeof slot.scene !== "string" || typeof slot.state !== "object" || slot.state === null) return null;
|
|
1333
|
+
return {
|
|
1334
|
+
scene: slot.scene,
|
|
1335
|
+
state: slot.state,
|
|
1336
|
+
label: typeof slot.label === "string" ? slot.label : "",
|
|
1337
|
+
savedAt: typeof slot.savedAt === "number" ? slot.savedAt : 0,
|
|
1338
|
+
playtime: typeof slot.playtime === "number" ? slot.playtime : 0,
|
|
1339
|
+
data: slot.data ?? {}
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Write a slot.
|
|
1344
|
+
*
|
|
1345
|
+
* `savedAt` is a parameter rather than `Date.now()` so a test can assert an
|
|
1346
|
+
* ordering without sleeping, and so a game that wants server time can use it.
|
|
1347
|
+
*/
|
|
1348
|
+
write(id, slot) {
|
|
1349
|
+
const full = {
|
|
1350
|
+
scene: slot.scene,
|
|
1351
|
+
state: slot.state,
|
|
1352
|
+
label: slot.label ?? "",
|
|
1353
|
+
savedAt: slot.savedAt ?? 0,
|
|
1354
|
+
playtime: slot.playtime ?? 0,
|
|
1355
|
+
data: slot.data ?? {}
|
|
1356
|
+
};
|
|
1357
|
+
this.store.set(key(id), full);
|
|
1358
|
+
const ids = this.store.get(INDEX_KEY, []);
|
|
1359
|
+
if (!ids.includes(id)) this.store.set(INDEX_KEY, [...ids, id]);
|
|
1360
|
+
}
|
|
1361
|
+
remove(id) {
|
|
1362
|
+
this.store.remove(key(id));
|
|
1363
|
+
this.store.set(INDEX_KEY, this.store.get(INDEX_KEY, []).filter((other) => other !== id));
|
|
1364
|
+
}
|
|
1365
|
+
/** Wipe every slot — a "delete all data" button, and test isolation. */
|
|
1366
|
+
clear() {
|
|
1367
|
+
for (const id of this.store.get(INDEX_KEY, [])) this.store.remove(key(id));
|
|
1368
|
+
this.store.set(INDEX_KEY, []);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
function key(id) {
|
|
1372
|
+
return `slot:${id}`;
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Behaviors in this tree that hold state and cannot save it — the audit's
|
|
1376
|
+
* question, answerable before a player loses an hour.
|
|
1377
|
+
*
|
|
1378
|
+
* A behavior with props but no `serialize` is not necessarily wrong (an
|
|
1379
|
+
* `Oscillate` derives everything from time), so this reports rather than warns,
|
|
1380
|
+
* and the caller decides.
|
|
1381
|
+
*/
|
|
1382
|
+
function behaviorsWithoutSave(root) {
|
|
1383
|
+
const out = [];
|
|
1384
|
+
const walk = (node) => {
|
|
1385
|
+
const behavior = node.behavior;
|
|
1386
|
+
const name = node.script?.name;
|
|
1387
|
+
if (behavior && typeof name === "string" && typeof behavior.serialize !== "function") {
|
|
1388
|
+
if (Object.keys(behaviorSchema(name)).length > 0) out.push(`${node.getPath()} (${name})`);
|
|
1389
|
+
}
|
|
1390
|
+
for (const child of node.children) walk(child);
|
|
1391
|
+
};
|
|
1392
|
+
walk(root);
|
|
1393
|
+
return out;
|
|
1394
|
+
}
|
|
1395
|
+
//#endregion
|
|
1074
1396
|
//#region src/core/settings.ts
|
|
1075
1397
|
const DEFAULTS = {
|
|
1076
1398
|
master: 1,
|
|
@@ -1080,7 +1402,8 @@ const DEFAULTS = {
|
|
|
1080
1402
|
quality: "high",
|
|
1081
1403
|
sensitivity: 1,
|
|
1082
1404
|
invertY: false,
|
|
1083
|
-
reduceMotion: false
|
|
1405
|
+
reduceMotion: false,
|
|
1406
|
+
language: "en"
|
|
1084
1407
|
};
|
|
1085
1408
|
const TIERS = [
|
|
1086
1409
|
"low",
|
|
@@ -1146,6 +1469,19 @@ var Settings = class {
|
|
|
1146
1469
|
for (const key of Object.keys(DEFAULTS)) this.set(key, DEFAULTS[key]);
|
|
1147
1470
|
}
|
|
1148
1471
|
/**
|
|
1472
|
+
* Apply the saved language, and keep it in step both ways.
|
|
1473
|
+
*
|
|
1474
|
+
* The same shape as `bindAudio`: a game that writes
|
|
1475
|
+
* `engine.locale.locale = 'ko'` from an options menu has already persisted it
|
|
1476
|
+
* without knowing this class exists. Returns the unsubscribe.
|
|
1477
|
+
*/
|
|
1478
|
+
bindLocale(locale) {
|
|
1479
|
+
locale.locale = this.values.language;
|
|
1480
|
+
return locale.changed.connect((next) => {
|
|
1481
|
+
this.set("language", next);
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1149
1485
|
* Push the saved volumes into the engine's buses, and keep writing them back
|
|
1150
1486
|
* whenever anything changes them.
|
|
1151
1487
|
*
|
|
@@ -1267,6 +1603,11 @@ var Engine = class {
|
|
|
1267
1603
|
* `bindAudio` makes the volumes write themselves.
|
|
1268
1604
|
*/
|
|
1269
1605
|
settings = new Settings();
|
|
1606
|
+
/**
|
|
1607
|
+
* Translations, and the locale in force. English is the base and the default;
|
|
1608
|
+
* anything a locale does not declare falls back to it silently.
|
|
1609
|
+
*/
|
|
1610
|
+
locale = new Localization();
|
|
1270
1611
|
/** Low-latency procedural-SFX player (WebAudio, headless-safe). AudioPlayer
|
|
1271
1612
|
* presets play through it; games may call `engine.sfx.play(...)` directly. */
|
|
1272
1613
|
sfx = new SfxEngine();
|
|
@@ -1340,6 +1681,7 @@ var Engine = class {
|
|
|
1340
1681
|
this._time = 0;
|
|
1341
1682
|
this._unscaledTime = 0;
|
|
1342
1683
|
if (scene.input) this.input.declare(scene.input);
|
|
1684
|
+
if (scene.strings) this.locale.load(scene.strings);
|
|
1343
1685
|
this.sceneChanged.emit(scene);
|
|
1344
1686
|
}
|
|
1345
1687
|
start() {
|
|
@@ -1377,6 +1719,41 @@ var Engine = class {
|
|
|
1377
1719
|
* both — so a tool can list them and put them back. Walks on demand; there is
|
|
1378
1720
|
* no per-frame bookkeeping for this.
|
|
1379
1721
|
*/
|
|
1722
|
+
/**
|
|
1723
|
+
* The string for `key` in the current locale — English if this locale does not
|
|
1724
|
+
* declare it, the key itself if nothing does.
|
|
1725
|
+
*
|
|
1726
|
+
* The short form of `engine.locale.t`, because a behavior reaching for a
|
|
1727
|
+
* translated string is the common case:
|
|
1728
|
+
* `banner.show(this.engine.t('wave.start', { n: wave }))`.
|
|
1729
|
+
*/
|
|
1730
|
+
t(key, params) {
|
|
1731
|
+
return this.locale.t(key, params);
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Every behavior's state, ready to put in a save slot.
|
|
1735
|
+
*
|
|
1736
|
+
* Pairs with `restoreState` after the scene has RELOADED — see
|
|
1737
|
+
* core/save-slots.ts for why a save carries state rather than a tree
|
|
1738
|
+
* snapshot.
|
|
1739
|
+
*/
|
|
1740
|
+
captureState() {
|
|
1741
|
+
const root = this.scene?.root;
|
|
1742
|
+
if (!root) return {};
|
|
1743
|
+
const { state, unaddressable } = captureBehaviors(root);
|
|
1744
|
+
for (const path of unaddressable) diagnose(this, "warn", `save: ${path} has state to save and no uid to key it under.`);
|
|
1745
|
+
return state;
|
|
1746
|
+
}
|
|
1747
|
+
/** Give it back, after the scene is loaded and onReady has run. */
|
|
1748
|
+
restoreState(state) {
|
|
1749
|
+
const root = this.scene?.root;
|
|
1750
|
+
if (!root) return {
|
|
1751
|
+
missing: Object.keys(state),
|
|
1752
|
+
skipped: [],
|
|
1753
|
+
restored: 0
|
|
1754
|
+
};
|
|
1755
|
+
return restoreBehaviors(root, state);
|
|
1756
|
+
}
|
|
1380
1757
|
erroredNodes() {
|
|
1381
1758
|
const out = [];
|
|
1382
1759
|
const walk = (node) => {
|
|
@@ -2010,6 +2387,18 @@ var HudWidgetBase = class extends Node {
|
|
|
2010
2387
|
this._sync();
|
|
2011
2388
|
}
|
|
2012
2389
|
_sync() {}
|
|
2390
|
+
/**
|
|
2391
|
+
* Resolve a text prop through the engine's locale.
|
|
2392
|
+
*
|
|
2393
|
+
* Called every frame from `_sync`, which is what makes switching language
|
|
2394
|
+
* repaint with nothing to invalidate: widgets already re-read their props, so
|
|
2395
|
+
* the new string simply appears on the next frame. A plain string costs one
|
|
2396
|
+
* `startsWith` and is returned untouched.
|
|
2397
|
+
*/
|
|
2398
|
+
_t(value) {
|
|
2399
|
+
const locale = this.tree?.engine?.locale;
|
|
2400
|
+
return locale ? locale.resolve(value) : value;
|
|
2401
|
+
}
|
|
2013
2402
|
};
|
|
2014
2403
|
/** A text line (score, timer, hints). Set `.text` from behaviors. */
|
|
2015
2404
|
var UiText = class extends HudWidgetBase {
|
|
@@ -2032,9 +2421,10 @@ var UiText = class extends HudWidgetBase {
|
|
|
2032
2421
|
return el;
|
|
2033
2422
|
}
|
|
2034
2423
|
_sync() {
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
this.
|
|
2424
|
+
const shown = this._t(this.text);
|
|
2425
|
+
if (shown !== this.last && this._element) {
|
|
2426
|
+
this.last = shown;
|
|
2427
|
+
this._element.textContent = shown;
|
|
2038
2428
|
}
|
|
2039
2429
|
}
|
|
2040
2430
|
};
|
|
@@ -2063,6 +2453,8 @@ var UiBar = class extends HudWidgetBase {
|
|
|
2063
2453
|
background = "rgba(0,0,0,0.5)";
|
|
2064
2454
|
label = "";
|
|
2065
2455
|
fill = null;
|
|
2456
|
+
tag = null;
|
|
2457
|
+
lastLabel = "\0";
|
|
2066
2458
|
lastRatio = -1;
|
|
2067
2459
|
/** Current fill ratio 0..1 (what the bar shows). */
|
|
2068
2460
|
get ratio() {
|
|
@@ -2073,8 +2465,8 @@ var UiBar = class extends HudWidgetBase {
|
|
|
2073
2465
|
wrap.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
2074
2466
|
if (this.label) {
|
|
2075
2467
|
const tag = document.createElement("span");
|
|
2076
|
-
tag.textContent = this.label;
|
|
2077
2468
|
tag.style.cssText = "color:#fff;font-size:12px;text-shadow:0 1px 3px rgba(0,0,0,.7);";
|
|
2469
|
+
this.tag = tag;
|
|
2078
2470
|
wrap.appendChild(tag);
|
|
2079
2471
|
}
|
|
2080
2472
|
const track = document.createElement("div");
|
|
@@ -2086,6 +2478,11 @@ var UiBar = class extends HudWidgetBase {
|
|
|
2086
2478
|
return wrap;
|
|
2087
2479
|
}
|
|
2088
2480
|
_sync() {
|
|
2481
|
+
const label = this._t(this.label);
|
|
2482
|
+
if (label !== this.lastLabel && this.tag) {
|
|
2483
|
+
this.lastLabel = label;
|
|
2484
|
+
this.tag.textContent = label;
|
|
2485
|
+
}
|
|
2089
2486
|
const r = this.ratio;
|
|
2090
2487
|
if (r === this.lastRatio || !this.fill) return;
|
|
2091
2488
|
this.lastRatio = r;
|
|
@@ -2163,6 +2560,7 @@ var UiBanner = class extends HudWidgetBase {
|
|
|
2163
2560
|
}
|
|
2164
2561
|
if (!this.current && this.queue.length > 0) {
|
|
2165
2562
|
this.current = this.queue.shift();
|
|
2563
|
+
this.current.text = this._t(this.current.text);
|
|
2166
2564
|
this.remaining = this.current.seconds;
|
|
2167
2565
|
if (this._element) {
|
|
2168
2566
|
this._element.textContent = this.current.text;
|
|
@@ -2231,9 +2629,10 @@ var UiButton = class extends HudWidgetBase {
|
|
|
2231
2629
|
}
|
|
2232
2630
|
_sync() {
|
|
2233
2631
|
if (!this._element) return;
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
this.
|
|
2632
|
+
const shown = this._t(this.text);
|
|
2633
|
+
if (shown !== this.lastText) {
|
|
2634
|
+
this.lastText = shown;
|
|
2635
|
+
this._element.textContent = shown;
|
|
2237
2636
|
}
|
|
2238
2637
|
this._element.disabled = this.disabled;
|
|
2239
2638
|
this._element.style.opacity = this.disabled ? "0.45" : "1";
|
|
@@ -2358,6 +2757,9 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
2358
2757
|
super.update(dt);
|
|
2359
2758
|
if (!this.current && this.queue.length > 0) {
|
|
2360
2759
|
this.current = this.queue.shift();
|
|
2760
|
+
this.current.text = this._t(this.current.text);
|
|
2761
|
+
this.current.speaker = this._t(this.current.speaker);
|
|
2762
|
+
if (this.current.choices) this.current.choices = this.current.choices.map((c) => this._t(c));
|
|
2361
2763
|
this.revealed = this.charsPerSecond <= 0 ? this.current.text.length : 0;
|
|
2362
2764
|
this.emit("lineShown", this.current.text);
|
|
2363
2765
|
if (this._element) {
|
|
@@ -2715,13 +3117,15 @@ var UiSelect = class extends HudWidgetBase {
|
|
|
2715
3117
|
this.emit("changed", next);
|
|
2716
3118
|
}
|
|
2717
3119
|
select = null;
|
|
3120
|
+
labelEl = null;
|
|
3121
|
+
lastLabel = "x";
|
|
2718
3122
|
lastOptions = "\0";
|
|
2719
3123
|
last = "\0";
|
|
2720
3124
|
_build() {
|
|
2721
3125
|
const wrap = document.createElement("label");
|
|
2722
3126
|
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;";
|
|
2723
3127
|
const text = document.createElement("span");
|
|
2724
|
-
|
|
3128
|
+
this.labelEl = text;
|
|
2725
3129
|
const select = document.createElement("select");
|
|
2726
3130
|
select.addEventListener("change", () => {
|
|
2727
3131
|
this.value = select.value;
|
|
@@ -2734,6 +3138,10 @@ var UiSelect = class extends HudWidgetBase {
|
|
|
2734
3138
|
this.fillOptions();
|
|
2735
3139
|
return wrap;
|
|
2736
3140
|
}
|
|
3141
|
+
/** Override point: what an option READS as, when it differs from its value. */
|
|
3142
|
+
_optionLabel(value) {
|
|
3143
|
+
return value;
|
|
3144
|
+
}
|
|
2737
3145
|
fillOptions() {
|
|
2738
3146
|
const select = this.select;
|
|
2739
3147
|
if (!select || this.options === this.lastOptions) return;
|
|
@@ -2744,13 +3152,18 @@ var UiSelect = class extends HudWidgetBase {
|
|
|
2744
3152
|
if (!value) continue;
|
|
2745
3153
|
const option = document.createElement("option");
|
|
2746
3154
|
option.value = value;
|
|
2747
|
-
option.textContent = value;
|
|
3155
|
+
option.textContent = this._optionLabel(value);
|
|
2748
3156
|
select.appendChild(option);
|
|
2749
3157
|
}
|
|
2750
3158
|
if (this.value) select.value = this.value;
|
|
2751
3159
|
this.last = this.value;
|
|
2752
3160
|
}
|
|
2753
3161
|
_sync() {
|
|
3162
|
+
const label = this._t(this.label);
|
|
3163
|
+
if (label !== this.lastLabel && this.labelEl) {
|
|
3164
|
+
this.lastLabel = label;
|
|
3165
|
+
this.labelEl.textContent = label;
|
|
3166
|
+
}
|
|
2754
3167
|
this.fillOptions();
|
|
2755
3168
|
if (this.select && this.value !== this.last) {
|
|
2756
3169
|
this.last = this.value;
|
|
@@ -2758,6 +3171,70 @@ var UiSelect = class extends HudWidgetBase {
|
|
|
2758
3171
|
}
|
|
2759
3172
|
}
|
|
2760
3173
|
};
|
|
3174
|
+
/** Endonyms: `ko` must read as the Korean word to someone who cannot read `Korean`. */
|
|
3175
|
+
const ENDONYMS = {
|
|
3176
|
+
en: "English",
|
|
3177
|
+
ko: "한국어",
|
|
3178
|
+
ja: "日本語",
|
|
3179
|
+
"zh-CN": "简体中文",
|
|
3180
|
+
"zh-TW": "繁體中文",
|
|
3181
|
+
es: "Español",
|
|
3182
|
+
fr: "Français",
|
|
3183
|
+
de: "Deutsch",
|
|
3184
|
+
pt: "Português",
|
|
3185
|
+
ru: "Русский",
|
|
3186
|
+
it: "Italiano",
|
|
3187
|
+
th: "ไทย",
|
|
3188
|
+
vi: "Tiếng Việt",
|
|
3189
|
+
id: "Bahasa Indonesia"
|
|
3190
|
+
};
|
|
3191
|
+
/**
|
|
3192
|
+
* The language picker, done — because every game needs the same one.
|
|
3193
|
+
*
|
|
3194
|
+
* A plain `UiSelect` bound to the locale would take a behavior, a signal
|
|
3195
|
+
* connection, and the knowledge that `Settings` persists it: three things an
|
|
3196
|
+
* agent has to get right to ship a feature the engine already has. This is one
|
|
3197
|
+
* node.
|
|
3198
|
+
*
|
|
3199
|
+
* ```json
|
|
3200
|
+
* { "name": "Language", "type": "UiLanguageSelect",
|
|
3201
|
+
* "props": { "label": "@t:settings.language" } }
|
|
3202
|
+
* ```
|
|
3203
|
+
*
|
|
3204
|
+
* Options are the locales the SCENE declares, so a game shipping only English
|
|
3205
|
+
* shows only English and the widget quietly costs nothing. Each is labeled with
|
|
3206
|
+
* its own endonym, because a player who cannot read the language currently on
|
|
3207
|
+
* screen still has to be able to find theirs.
|
|
3208
|
+
*/
|
|
3209
|
+
var UiLanguageSelect = class extends UiSelect {
|
|
3210
|
+
static typeName = "UiLanguageSelect";
|
|
3211
|
+
static props = {
|
|
3212
|
+
...UiSelect.props,
|
|
3213
|
+
label: { default: "Language" }
|
|
3214
|
+
};
|
|
3215
|
+
label = "Language";
|
|
3216
|
+
/** Last locale we PUSHED into `value`, to tell an engine change from a player one. */
|
|
3217
|
+
lastLocale = "";
|
|
3218
|
+
_sync() {
|
|
3219
|
+
const locale = this.tree?.engine?.locale;
|
|
3220
|
+
if (locale) {
|
|
3221
|
+
const available = locale.available();
|
|
3222
|
+
const options = available.join(",");
|
|
3223
|
+
if (options !== this.options) this.options = options;
|
|
3224
|
+
if (locale.locale !== this.lastLocale) {
|
|
3225
|
+
this.lastLocale = locale.locale;
|
|
3226
|
+
this.value = locale.locale;
|
|
3227
|
+
} else if (this.value !== locale.locale && available.includes(this.value)) {
|
|
3228
|
+
locale.locale = this.value;
|
|
3229
|
+
this.lastLocale = this.value;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
super._sync();
|
|
3233
|
+
}
|
|
3234
|
+
_optionLabel(value) {
|
|
3235
|
+
return ENDONYMS[value] ?? value;
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
2761
3238
|
//#endregion
|
|
2762
3239
|
//#region src/core/register.ts
|
|
2763
3240
|
/**
|
|
@@ -2780,6 +3257,7 @@ function registerCoreNodes() {
|
|
|
2780
3257
|
registerNode(UiSlider);
|
|
2781
3258
|
registerNode(UiToggle);
|
|
2782
3259
|
registerNode(UiSelect);
|
|
3260
|
+
registerNode(UiLanguageSelect);
|
|
2783
3261
|
}
|
|
2784
3262
|
//#endregion
|
|
2785
|
-
export {
|
|
3263
|
+
export { translationKey as A, WebAudioMusicBackend as B, restoreBehaviors as C, Localization as D, BASE_LOCALE as E, spatialPan as F, fadeGain as H, SFX_PRESETS as I, SFX_PRESET_NAMES as L, isAudioContextAvailable as M, ROLLOFF_MODELS as N, T_PREFIX as O, spatialGain as P, synthSfx as R, captureBehaviors as S, LogManager as T, AudioBuses as U, crossfadeGains as V, qualityEnvironment as _, UiSlider as a, SaveSlots as b, UiButton as c, UiBanner as d, UiBar as f, Settings as g, Engine as h, UiSelect as i, SfxEngine as j, suggestLocale as k, UiDialogue as l, AudioPlayer as m, UiImage as n, UiToggle as o, UiText as p, UiPanel as r, Timer as s, registerCoreNodes as t, HudLayer as u, readDeviceHints as v, createSaveStore as w, behaviorsWithoutSave as x, suggestQuality as y, MusicManager as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
1
|
+
import { C as Signal, f as Node, t as buildNodeJson } from "./loader-Mig5fY4n.js";
|
|
2
|
+
import { t as registerCoreNodes } from "./register-BFLg0-_i.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-CJdGpT2V.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { f as Node } from "./loader-
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
1
|
+
import { f as Node } from "./loader-Mig5fY4n.js";
|
|
2
|
+
import { t as registerCoreNodes } from "./register-BFLg0-_i.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-CJdGpT2V.js";
|
|
6
|
-
import { r as effectiveOrder } from "./duplicate-
|
|
6
|
+
import { r as effectiveOrder } from "./duplicate-C716f-97.js";
|
|
7
7
|
import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
|
|
8
8
|
import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry } from "three";
|
|
9
9
|
//#region src/2d/physics/collider-validate.ts
|
|
@@ -266,7 +266,10 @@ var Joint2D = class extends Node2D {
|
|
|
266
266
|
"spring"
|
|
267
267
|
]
|
|
268
268
|
},
|
|
269
|
-
target: {
|
|
269
|
+
target: {
|
|
270
|
+
default: "",
|
|
271
|
+
nodePath: true
|
|
272
|
+
},
|
|
270
273
|
anchor: { default: [0, 0] },
|
|
271
274
|
targetAnchor: { default: [0, 0] },
|
|
272
275
|
length: { default: 0 },
|
|
@@ -482,7 +485,10 @@ function resolveFrames(frames, name) {
|
|
|
482
485
|
var Camera2D = class extends Node2D {
|
|
483
486
|
static typeName = "Camera2D";
|
|
484
487
|
static props = {
|
|
485
|
-
follow: {
|
|
488
|
+
follow: {
|
|
489
|
+
default: "",
|
|
490
|
+
nodePath: true
|
|
491
|
+
},
|
|
486
492
|
smoothing: { default: 0 },
|
|
487
493
|
zoom: { default: 1 },
|
|
488
494
|
limits: { default: [] },
|