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.
Files changed (58) hide show
  1. package/dist/2d.d.ts +5 -5
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +122 -14
  4. package/dist/3d.js +5 -5
  5. package/dist/{audit-C4kmDK0o.js → audit-C5oZGAru.js} +37 -0
  6. package/dist/{behavior-CtPScDMo.d.ts → behavior-CGSCWOHB.d.ts} +310 -22
  7. package/dist/{create-game-CFNqoqOX.js → create-game-B0Wfhi2-.js} +6 -5
  8. package/dist/{create-game-QTfghzwM.js → create-game-BUD89Kqh.js} +20 -6
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DlOvknDO.js → duplicate-C716f-97.js} +1 -1
  11. package/dist/{editor-switch-BJb-CWfA.d.ts → editor-switch-DyXEtH36.d.ts} +1 -1
  12. package/dist/editor.js +1739 -1312
  13. package/dist/env.d.ts +1 -1
  14. package/dist/{environment-presets-B6WiUsaO.js → environment-presets-TAGvmM3z.js} +240 -74
  15. package/dist/{errors-1dXlIwoR.d.ts → errors-BY2kL0hv.d.ts} +1 -1
  16. package/dist/{gameplay-BxFtmfSe.js → gameplay-BvhcQbfJ.js} +75 -11
  17. package/dist/gameplay.d.ts +12 -5
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +22 -7
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-DmXfj6Uv.d.ts → loader-CUcj00M8.d.ts} +19 -2
  22. package/dist/{loader-BwR0DxeM.js → loader-Mig5fY4n.js} +97 -18
  23. package/dist/net.d.ts +2 -2
  24. package/dist/net.js +3 -3
  25. package/dist/{particle-sim-CwJ5rI_P.d.ts → particle-sim-B-vZBF5R.d.ts} +1 -1
  26. package/dist/{pathfinding-DRCe89SI.d.ts → pathfinding-DgOo2KNF.d.ts} +1 -1
  27. package/dist/{physics-2d-BuyAwsW5.js → physics-2d-DqAclql-.js} +22 -9
  28. package/dist/{physics-3d-C8Kn_Nyb.js → physics-3d-DxBH4sIF.js} +27 -11
  29. package/dist/react.d.ts +2 -2
  30. package/dist/react.js +1 -1
  31. package/dist/{register-G3edsjJ2.js → register-BFLg0-_i.js} +490 -12
  32. package/dist/{register-OodmuUMK.js → register-BjbPMA5B.js} +2 -2
  33. package/dist/{register-BLPC10Mj.js → register-DSmIRAf7.js} +11 -5
  34. package/dist/{schema-CcoWb32N.d.ts → schema-3ywbdlrv.d.ts} +10 -0
  35. package/dist/{test-C4B5znap.js → test-9EokzbRd.js} +10 -10
  36. package/dist/test.d.ts +4 -4
  37. package/dist/test.js +2 -2
  38. package/dist/vite.js +1 -1
  39. package/editor/assets/{agent8-BTODHtdM.js → agent8-D3_GWeuh.js} +1 -1
  40. package/editor/assets/{debug-CJEz4EwC.js → debug-CX0LDd1r.js} +1 -1
  41. package/editor/assets/index-CMlKFT0C.js +10773 -0
  42. package/editor/index.html +1 -1
  43. package/package.json +1 -1
  44. package/schemas/scene.schema.json +109 -0
  45. package/skills/incanto-assets.md +21 -0
  46. package/skills/incanto-behaviors-and-scripts.md +16 -0
  47. package/skills/incanto-editor.md +17 -1
  48. package/skills/incanto-gameplay-behaviors.md +6 -0
  49. package/skills/incanto-hud.md +16 -0
  50. package/skills/incanto-localization.md +132 -0
  51. package/skills/incanto-node-reference.md +33 -18
  52. package/skills/incanto-save-slots.md +152 -0
  53. package/templates-app/beacon-isle-3d/package.json +1 -1
  54. package/templates-app/tps-3d/package.json +1 -1
  55. package/templates-app/village-quest-3d/PROJECT/Context.md +16 -0
  56. package/templates-app/village-quest-3d/package.json +1 -1
  57. package/templates-app/village-quest-3d/src/village.scene.json +25 -3
  58. package/editor/assets/index-BYCso8Bf.js +0 -10696
@@ -1,4 +1,4 @@
1
- import { a as Rng, c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
1
+ import { a as Rng, c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-3ywbdlrv.js";
2
2
 
3
3
  //#region src/core/signal.d.ts
4
4
  /**
@@ -80,6 +80,35 @@ declare class SceneTree {
80
80
  * change shape?" check for per-step body gathers and similar caches.
81
81
  */
82
82
  _structureVersion: number;
83
+ /**
84
+ * WHAT changed, not just that something did.
85
+ *
86
+ * The version alone forces every consumer into a full-tree re-walk, and the
87
+ * physics step is gated on it: one bullet spawned per frame means
88
+ * `collectBodies` + `collectJoints` + `syncScatters` walk the entire scene at
89
+ * 60 Hz. A bullet-hell or a survivors-like does nothing else all game.
90
+ *
91
+ * This log lets a consumer replay just the attaches and detaches it has not
92
+ * seen. It is bounded — a scene load appends thousands of entries and nobody
93
+ * benefits from replaying those — so a consumer that falls behind
94
+ * `_structureLogBase` takes the full walk, exactly as before. Correctness does
95
+ * not depend on the log; only the fast path does.
96
+ */
97
+ private _structureLog;
98
+ /** The `_structureVersion` the first entry in the log corresponds to. */
99
+ private _structureLogBase;
100
+ private static readonly STRUCTURE_LOG_MAX;
101
+ /**
102
+ * Attaches and detaches since `sinceVersion`, or null when the log no longer
103
+ * reaches back that far (take the full walk).
104
+ *
105
+ * The returned array is the live buffer's tail — do not hold it across frames.
106
+ */
107
+ _structureSince(sinceVersion: number): ReadonlyArray<{
108
+ node: Node;
109
+ added: boolean;
110
+ }> | null;
111
+ private _logStructure;
83
112
  private _frameId;
84
113
  /**
85
114
  * Monotonic update-frame counter — bumped once per `update()`. Per-frame
@@ -89,7 +118,15 @@ declare class SceneTree {
89
118
  get frameId(): number;
90
119
  /** @internal Node attach hook. */
91
120
  _indexName(node: Node): void;
92
- /** @internal Node detach/rename hook (`name` = the entry to remove). */
121
+ /**
122
+ * @internal A RENAME: same node, same place, different key in the name index.
123
+ *
124
+ * Deliberately not logged as a detach + attach. It is not a structural change,
125
+ * and treating it as one would make the physics step tear down and rebuild the
126
+ * node's Rapier body for a name edit.
127
+ */
128
+ _reindexName(node: Node, oldName: string): void;
129
+ /** @internal Node detach hook (`name` = the entry to remove). */
93
130
  _unindexName(node: Node, name?: string): void;
94
131
  /** @internal Every ATTACHED node named `name` (undefined = none). */
95
132
  _nodesNamed(name: string): ReadonlySet<Node> | undefined;
@@ -763,6 +800,184 @@ declare class InputMap {
763
800
  private button;
764
801
  }
765
802
  //#endregion
803
+ //#region src/core/localization.d.ts
804
+ /** The base locale. Every other locale falls back to it, one key at a time. */
805
+ declare const BASE_LOCALE = "en";
806
+ /**
807
+ * The marker that makes a string prop translatable: `"@t:hud.score"`.
808
+ *
809
+ * A prefix rather than an object wrapper, matching the `"$assetKey"` reference
810
+ * already in the engine — the prop is declared a string, so its value should
811
+ * stay a string. It also stays authorable by hand:
812
+ *
813
+ * ```json
814
+ * { "type": "UiText", "props": { "text": "@t:hud.score" } }
815
+ * ```
816
+ */
817
+ declare const T_PREFIX = "@t:";
818
+ /** `locale → key → string`. A locale may declare any subset of the base's keys. */
819
+ type LocaleTables = Record<string, Record<string, string>>;
820
+ /** The key a `"@t:…"` value names, or null for an ordinary string. */
821
+ declare function translationKey(value: string): string | null;
822
+ declare class Localization {
823
+ /** Fires with the new locale after it changes — for anything that caches. */
824
+ readonly changed: Signal<[string]>;
825
+ private tables;
826
+ private current;
827
+ constructor(tables?: LocaleTables, locale?: string);
828
+ /**
829
+ * Merge a scene's `strings` table in.
830
+ *
831
+ * Merged rather than replaced: a game's shared UI strings live in the scene it
832
+ * boots from, and a level that adds three lines of its own should not wipe
833
+ * them. Per-key, so a level CAN override one string.
834
+ */
835
+ load(tables: LocaleTables): void;
836
+ get locale(): string;
837
+ /**
838
+ * Switch language. Setting one nobody declared is not an error — it simply
839
+ * resolves everything through the base locale, which is the correct outcome
840
+ * for a player whose browser reports a language this game does not ship.
841
+ */
842
+ set locale(next: string);
843
+ /** Every locale with at least one string — what a language picker offers. */
844
+ available(): string[];
845
+ /**
846
+ * The string for `key`: current locale, else English, else the key itself.
847
+ *
848
+ * Returning the key rather than an empty string is deliberate — a screen
849
+ * reading `menu.credits` tells an author exactly what to add, where a blank
850
+ * one tells them nothing and looks like a rendering bug.
851
+ */
852
+ t(key: string, params?: Record<string, JsonValue>): string;
853
+ /**
854
+ * Resolve a prop value: `"@t:key"` translates, anything else passes through.
855
+ *
856
+ * This is what every text-bearing widget calls, every frame. A plain string is
857
+ * returned untouched, so a game that never localizes anything pays a
858
+ * `startsWith` per widget per frame and nothing else.
859
+ */
860
+ resolve(value: string, params?: Record<string, JsonValue>): string;
861
+ /** Whether any locale declares this key — for the audit, not for the runtime. */
862
+ has(key: string): boolean;
863
+ }
864
+ /**
865
+ * The player's language, if the browser will say and the game ships it.
866
+ *
867
+ * A starting point only — the language setting is the real answer, exactly as
868
+ * `suggestQuality` is a starting point for the quality tier.
869
+ */
870
+ declare function suggestLocale(available: readonly string[], languages?: readonly string[]): string;
871
+ //#endregion
872
+ //#region src/core/save.d.ts
873
+ /**
874
+ * Namespaced game persistence — high scores, unlocks, settings. localStorage
875
+ * in the browser, in-memory headless (tests and SSR stay green). Values are
876
+ * JSON round-tripped, so what you get back is what you saved.
877
+ *
878
+ * const save = createSaveStore('my-game');
879
+ * save.set('highScore', 4200);
880
+ * const best = save.get('highScore', 0);
881
+ */
882
+ interface SaveStore {
883
+ get<T extends JsonValue>(key: string, fallback: T): T;
884
+ set(key: string, value: JsonValue): void;
885
+ remove(key: string): void;
886
+ /** Wipe THIS namespace only. */
887
+ clear(): void;
888
+ }
889
+ declare function createSaveStore(namespace: string): SaveStore;
890
+ //#endregion
891
+ //#region src/core/save-slots.d.ts
892
+ /** Behavior state, keyed by the uid of the node carrying it. */
893
+ type BehaviorState = Record<string, JsonValue>;
894
+ interface SaveSlot {
895
+ /** Which scene to load before restoring. Whatever key your game routes on. */
896
+ scene: string;
897
+ /** `uid → whatever that behavior's serialize() returned`. */
898
+ state: BehaviorState;
899
+ /** Shown in a load menu. Free text — "Chapter 2 · Emberwood". */
900
+ label: string;
901
+ /** Unix ms. Passed in, never read from a clock here, so saves stay testable. */
902
+ savedAt: number;
903
+ /** Seconds played, if the game tracks it. */
904
+ playtime: number;
905
+ /** Anything else the game wants in the slot (difficulty, seed, a thumbnail). */
906
+ data: JsonObject;
907
+ }
908
+ /** What a restore could not place, so a game can decide whether that matters. */
909
+ interface RestoreReport {
910
+ /** Entries whose uid is not in this scene — a node deleted since the save. */
911
+ missing: string[];
912
+ /** Entries whose node has no behavior, or one that cannot deserialize. */
913
+ skipped: string[];
914
+ /** How many behaviors actually took their state back. */
915
+ restored: number;
916
+ }
917
+ /**
918
+ * Every behavior in the tree that has something to say, by uid.
919
+ *
920
+ * A behavior with no `serialize` contributes nothing — the hook is optional in
921
+ * exactly the way the other five are, so adding save support to a game is
922
+ * additive and a game that never saves pays nothing.
923
+ *
924
+ * A node with no `uid` is skipped and reported, because state keyed by nothing
925
+ * cannot be given back. The editor assigns a uid to every node it touches; a
926
+ * hand-written scene may not have.
927
+ */
928
+ declare function captureBehaviors(root: Node): {
929
+ state: BehaviorState; /** Paths of nodes that had state to save and no uid to save it under. */
930
+ unaddressable: string[];
931
+ };
932
+ /**
933
+ * Hand each behavior its state back.
934
+ *
935
+ * Call this AFTER the scene has loaded and `onReady` has run: `onReady` is where
936
+ * a behavior sets its starting values, so restoring first would be overwritten
937
+ * by the fresh start.
938
+ *
939
+ * Never throws on a mismatch. A save from an older build of the game will name
940
+ * uids that no longer exist, and refusing to load it would mean a patch that
941
+ * moves one node deletes everyone's progress. It reports instead.
942
+ */
943
+ declare function restoreBehaviors(root: Node, state: BehaviorState): RestoreReport;
944
+ /**
945
+ * The slot layer: a named list of saves over the existing store.
946
+ *
947
+ * Slot ids are yours (`'auto'`, `'1'`, `'2'`, `'3'`) — the engine does not
948
+ * decide how many you have or whether one of them autosaves.
949
+ */
950
+ declare class SaveSlots {
951
+ private readonly store;
952
+ constructor(namespace?: string, store?: SaveStore);
953
+ /** Slot ids that have a save, newest first. */
954
+ list(): string[];
955
+ /** Every slot with its contents — what a load menu actually renders. */
956
+ all(): Array<SaveSlot & {
957
+ id: string;
958
+ }>;
959
+ read(id: string): SaveSlot | null;
960
+ /**
961
+ * Write a slot.
962
+ *
963
+ * `savedAt` is a parameter rather than `Date.now()` so a test can assert an
964
+ * ordering without sleeping, and so a game that wants server time can use it.
965
+ */
966
+ write(id: string, slot: Omit<SaveSlot, "label" | "savedAt" | "playtime" | "data"> & Partial<SaveSlot>): void;
967
+ remove(id: string): void;
968
+ /** Wipe every slot — a "delete all data" button, and test isolation. */
969
+ clear(): void;
970
+ }
971
+ /**
972
+ * Behaviors in this tree that hold state and cannot save it — the audit's
973
+ * question, answerable before a player loses an hour.
974
+ *
975
+ * A behavior with props but no `serialize` is not necessarily wrong (an
976
+ * `Oscillate` derives everything from time), so this reports rather than warns,
977
+ * and the caller decides.
978
+ */
979
+ declare function behaviorsWithoutSave(root: Node): string[];
980
+ //#endregion
766
981
  //#region src/core/scene/scene.d.ts
767
982
  /**
768
983
  * A loaded, live scene: the node tree plus everything needed to round-trip
@@ -786,6 +1001,8 @@ declare class Scene {
786
1001
  readonly constants: Record<string, JsonValue> | undefined;
787
1002
  /** Input action declarations (loaded into `Engine.input` on setScene). */
788
1003
  readonly input: JsonObject | undefined;
1004
+ /** Translations by locale (merged into `Engine.locale` on setScene). */
1005
+ readonly strings: Record<string, Record<string, string>> | undefined;
789
1006
  /** Physics config (gravity etc. — interpreted by the physics modules). */
790
1007
  readonly physics: JsonObject | undefined;
791
1008
  /** Multiplayer config (room etc. — interpreted by incanto/net). */
@@ -800,25 +1017,6 @@ declare class Scene {
800
1017
  toJSON(): SceneJson;
801
1018
  }
802
1019
  //#endregion
803
- //#region src/core/save.d.ts
804
- /**
805
- * Namespaced game persistence — high scores, unlocks, settings. localStorage
806
- * in the browser, in-memory headless (tests and SSR stay green). Values are
807
- * JSON round-tripped, so what you get back is what you saved.
808
- *
809
- * const save = createSaveStore('my-game');
810
- * save.set('highScore', 4200);
811
- * const best = save.get('highScore', 0);
812
- */
813
- interface SaveStore {
814
- get<T extends JsonValue>(key: string, fallback: T): T;
815
- set(key: string, value: JsonValue): void;
816
- remove(key: string): void;
817
- /** Wipe THIS namespace only. */
818
- clear(): void;
819
- }
820
- declare function createSaveStore(namespace: string): SaveStore;
821
- //#endregion
822
1020
  //#region src/core/settings.d.ts
823
1021
  /** One of three tiers, resolved once at boot and adjustable in an options menu. */
824
1022
  type QualityTier = "low" | "medium" | "high";
@@ -833,6 +1031,11 @@ interface SettingsValues {
833
1031
  invertY: boolean;
834
1032
  /** Reduce camera shake, flashes and other motion. */
835
1033
  reduceMotion: boolean;
1034
+ /**
1035
+ * The player's language. `'en'` is the base and the default; anything a locale
1036
+ * does not translate falls back to English, silently and on purpose.
1037
+ */
1038
+ language: string;
836
1039
  }
837
1040
  /** What a device probe can see without a GPU context. */
838
1041
  interface DeviceHints {
@@ -864,6 +1067,14 @@ declare class Settings {
864
1067
  /** Back to defaults, on disk too. */
865
1068
  reset(): void;
866
1069
  /**
1070
+ * Apply the saved language, and keep it in step both ways.
1071
+ *
1072
+ * The same shape as `bindAudio`: a game that writes
1073
+ * `engine.locale.locale = 'ko'` from an options menu has already persisted it
1074
+ * without knowing this class exists. Returns the unsubscribe.
1075
+ */
1076
+ bindLocale(locale: Localization): () => void;
1077
+ /**
867
1078
  * Push the saved volumes into the engine's buses, and keep writing them back
868
1079
  * whenever anything changes them.
869
1080
  *
@@ -975,6 +1186,11 @@ declare class Engine {
975
1186
  * `bindAudio` makes the volumes write themselves.
976
1187
  */
977
1188
  readonly settings: Settings;
1189
+ /**
1190
+ * Translations, and the locale in force. English is the base and the default;
1191
+ * anything a locale does not declare falls back to it silently.
1192
+ */
1193
+ readonly locale: Localization;
978
1194
  /** Low-latency procedural-SFX player (WebAudio, headless-safe). AudioPlayer
979
1195
  * presets play through it; games may call `engine.sfx.play(...)` directly. */
980
1196
  readonly sfx: SfxEngine;
@@ -1045,6 +1261,25 @@ declare class Engine {
1045
1261
  * both — so a tool can list them and put them back. Walks on demand; there is
1046
1262
  * no per-frame bookkeeping for this.
1047
1263
  */
1264
+ /**
1265
+ * The string for `key` in the current locale — English if this locale does not
1266
+ * declare it, the key itself if nothing does.
1267
+ *
1268
+ * The short form of `engine.locale.t`, because a behavior reaching for a
1269
+ * translated string is the common case:
1270
+ * `banner.show(this.engine.t('wave.start', { n: wave }))`.
1271
+ */
1272
+ t(key: string, params?: Record<string, JsonValue>): string;
1273
+ /**
1274
+ * Every behavior's state, ready to put in a save slot.
1275
+ *
1276
+ * Pairs with `restoreState` after the scene has RELOADED — see
1277
+ * core/save-slots.ts for why a save carries state rather than a tree
1278
+ * snapshot.
1279
+ */
1280
+ captureState(): BehaviorState;
1281
+ /** Give it back, after the scene is loaded and onReady has run. */
1282
+ restoreState(state: BehaviorState): RestoreReport;
1048
1283
  erroredNodes(): Node[];
1049
1284
  /** Un-skip every quarantined node — the "try again" after you fix the code. */
1050
1285
  resumeErroredNodes(): number;
@@ -1091,6 +1326,16 @@ interface PropDef {
1091
1326
  * dropdowns and the generated JSON-Schema enum — keep it exhaustive. */
1092
1327
  options?: readonly string[];
1093
1328
  /**
1329
+ * This prop holds a NODE PATH (`%Unique`, `/Absolute/From/Root`, or one
1330
+ * relative to the node that owns the prop) resolved with `getNodeOrNull`.
1331
+ *
1332
+ * Declaring it is what lets a tool rewrite the reference when the target is
1333
+ * renamed. Without it a rename silently breaks the link — the resolve returns
1334
+ * null and the game keeps running with an enemy that never chases anyone.
1335
+ * Set it on every prop you feed to `getNode`/`getNodeOrNull`.
1336
+ */
1337
+ nodePath?: boolean;
1338
+ /**
1094
1339
  * For an OBJECT-valued prop shaped as a tagged union — `collider`, whose
1095
1340
  * `shape` decides which other keys are required. One declaration gives an
1096
1341
  * inspector both the dropdown AND something valid to write when the tag
@@ -1173,14 +1418,57 @@ declare abstract class Behavior {
1173
1418
  onExitTree?(): void;
1174
1419
  update?(dt: number): void;
1175
1420
  fixedUpdate?(dt: number): void;
1421
+ /**
1422
+ * What this behavior needs in a save file — the sixth optional hook, and the
1423
+ * one that made "continue where you left off" possible at all.
1424
+ *
1425
+ * Return the state a fresh `onReady` could NOT recreate: current health,
1426
+ * accumulated score, which quest flags are set. Do not return anything the
1427
+ * scene JSON already says (`maxHealth`, `speed`) — that comes back from the
1428
+ * file, and duplicating it just makes old saves fight new balance patches.
1429
+ *
1430
+ * Must be plain JSON. Never a Node, a Texture, or a callback.
1431
+ *
1432
+ * ```ts
1433
+ * override serialize() { return { current: this.current }; }
1434
+ * override deserialize(data: JsonValue) {
1435
+ * const d = data as { current?: number };
1436
+ * if (typeof d.current === 'number') this.current = d.current;
1437
+ * }
1438
+ * ```
1439
+ *
1440
+ * Returning `undefined` writes nothing, so a behavior can decline to save on
1441
+ * a given frame (a boss that is mid-death-animation, say).
1442
+ */
1443
+ serialize?(): JsonValue | undefined;
1444
+ /**
1445
+ * Take that state back, AFTER `onReady` has run.
1446
+ *
1447
+ * Defensive on purpose: this data may come from a save written by an older
1448
+ * build of the game. Check what you read; do not assume shape. Throwing here
1449
+ * costs only this one behavior's state — the restore keeps going — but
1450
+ * quietly ignoring a field you renamed is usually the better answer.
1451
+ */
1452
+ deserialize?(data: JsonValue): void;
1176
1453
  }
1177
1454
  /** Explicit registration — never an import side effect (tree-shaking safety). */
1178
1455
  declare function registerBehavior(name: string, ctor: BehaviorCtor, opts?: {
1179
1456
  replace?: boolean;
1180
1457
  }): void;
1181
1458
  declare function getBehavior(name: string): BehaviorCtor;
1459
+ /**
1460
+ * A registered behavior's merged prop schema, `{}` for one nobody registered.
1461
+ *
1462
+ * The node side has had `getNodeSchema` for a long time; a behavior's props were
1463
+ * reachable only by whoever already held the class, which a tool reading scene
1464
+ * JSON never does. Deliberately non-throwing: a tool inspecting a scene that
1465
+ * names a behavior from a game it did not load should degrade, not blow up.
1466
+ */
1467
+ declare function behaviorSchema(name: string): PropSchema;
1468
+ /** A registered behavior's signals (own + inherited), `[]` for an unknown one. */
1469
+ declare function behaviorSignals(name: string): string[];
1182
1470
  declare function registeredBehaviors(): string[];
1183
1471
  /** Test isolation helper. */
1184
1472
  declare function clearBehaviors(): void;
1185
1473
  //#endregion
1186
- export { MusicBackend as $, suggestQuality as A, Listener as B, RendererStats as C, SettingsValues as D, Settings as E, SfxEngine as F, spatialGain as G, RolloffModel as H, SfxPlayOptions as I, SFX_PRESET_NAMES as J, spatialPan as K, isAudioContextAvailable as L, createSaveStore as M, Scene as N, qualityEnvironment as O, InputMap as P, synthSfx as Q, Voice as R, GameStats as S, QualityTier as T, SpatialParams as U, ROLLOFF_MODELS as V, Vec3 as W, SfxWave as X, SfxParams as Y, SynthOptions as Z, registeredTypes as _, registerBehavior as a, Node as at, Scheduler as b, PropDef as c, LogEntry as ct, createNode as d, Signal as dt, MusicManager as et, getNodeSchema as f, SignalListener as ft, registerNode as g, mergeStaticSignals as h, getBehavior as i, BusName as it, SaveStore as j, readDeviceHints as k, PropSchema as l, LogLevel as lt, getNodeType as m, BehaviorCtor as n, PlayMusicOptions as nt, registeredBehaviors as o, NodeLifecycle as ot, getNodeSignals as p, SFX_PRESETS as q, clearBehaviors as r, AudioBuses as rt, NodeCtor as s, SceneTree as st, Behavior as t, MusicTrack as tt, clearRegistry as u, LogManager as ut, Engine as v, DeviceHints as w, EngineStats as x, EngineOptions as y, VoicePreset as z };
1474
+ export { Voice as $, qualityEnvironment as A, restoreBehaviors as B, EngineStats as C, LogEntry as Ct, QualityTier as D, SignalListener as Dt, DeviceHints as E, Signal as Et, RestoreReport as F, Localization as G, createSaveStore as H, SaveSlot as I, translationKey as J, T_PREFIX as K, SaveSlots as L, suggestQuality as M, Scene as N, Settings as O, BehaviorState as P, isAudioContextAvailable as Q, behaviorsWithoutSave as R, Scheduler as S, SceneTree as St, RendererStats as T, LogManager as Tt, BASE_LOCALE as U, SaveStore as V, LocaleTables as W, SfxEngine as X, InputMap as Y, SfxPlayOptions as Z, mergeStaticSignals as _, PlayMusicOptions as _t, clearBehaviors as a, Vec3 as at, Engine as b, Node as bt, registeredBehaviors as c, SFX_PRESETS as ct, PropSchema as d, SfxWave as dt, VoicePreset as et, clearRegistry as f, SynthOptions as ft, getNodeType as g, MusicTrack as gt, getNodeSignals as h, MusicManager as ht, behaviorSignals as i, SpatialParams as it, readDeviceHints as j, SettingsValues as k, NodeCtor as l, SFX_PRESET_NAMES as lt, getNodeSchema as m, MusicBackend as mt, BehaviorCtor as n, ROLLOFF_MODELS as nt, getBehavior as o, spatialGain as ot, createNode as p, synthSfx as pt, suggestLocale as q, behaviorSchema as r, RolloffModel as rt, registerBehavior as s, spatialPan as st, Behavior as t, Listener as tt, PropDef as u, SfxParams as ut, registerNode as v, AudioBuses as vt, GameStats as w, LogLevel as wt, EngineOptions as x, NodeLifecycle as xt, registeredTypes as y, BusName as yt, captureBehaviors as z };
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { m as diagnose, n as loadScene, o as computeViewport, s as resolveViewport, y as registerBehavior } from "./loader-BwR0DxeM.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-G3edsjJ2.js";
2
+ import { h as diagnose, n as loadScene, o as computeViewport, s as resolveViewport, x as registerBehavior } from "./loader-Mig5fY4n.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-BFLg0-_i.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-BxFtmfSe.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BLPC10Mj.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BvhcQbfJ.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DSmIRAf7.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-BuyAwsW5.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-DqAclql-.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -545,6 +545,7 @@ async function createGame2D(opts) {
545
545
  if (keyboard) engine.input.attachKeyboard(keyboard);
546
546
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
547
547
  cleanups.push(engine.settings.bindAudio(engine.audio));
548
+ cleanups.push(engine.settings.bindLocale(engine.locale));
548
549
  const touchMode = opts.touch ?? "auto";
549
550
  if (touchMode !== false) {
550
551
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { m as diagnose, n as loadScene, y as registerBehavior } from "./loader-BwR0DxeM.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-G3edsjJ2.js";
2
+ import { h as diagnose, n as loadScene, x as registerBehavior } from "./loader-Mig5fY4n.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-BFLg0-_i.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { a as poseFromRenderer, i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BxFtmfSe.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BvhcQbfJ.js";
8
8
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
- import { E as Camera3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-B6WiUsaO.js";
10
- import { n as enablePhysics3D } from "./physics-3d-C8Kn_Nyb.js";
9
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-TAGvmM3z.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-DxBH4sIF.js";
11
11
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
12
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -32,6 +32,12 @@ var AssetStore3D = class {
32
32
  animationKeys = /* @__PURE__ */ new Map();
33
33
  loader;
34
34
  /**
35
+ * Shared textures, store-scoped like everything else here. Instance state, not
36
+ * a module map: two games on one page must not share a decode cache, and the
37
+ * repo has one rule about module-level engine state.
38
+ */
39
+ textures;
40
+ /**
35
41
  * `engine` is optional only so a bare store still constructs in tests. Give it
36
42
  * one and every load failure lands in `engine.log` — where the debug overlay,
37
43
  * `runScript()` and any agent tool can see it. Without it a 404'd GLB was a
@@ -41,6 +47,7 @@ var AssetStore3D = class {
41
47
  this.engine = engine;
42
48
  this.loader = new GLTFLoader();
43
49
  this.loader.register((parser) => new VRMLoaderPlugin(parser));
50
+ this.textures = new TextureCache3D({ diagnostics: engine ? { warn: (...parts) => diagnose(engine, "warn", ...parts) } : null });
44
51
  }
45
52
  /**
46
53
  * Every asset that failed, with the reason — the answer to "why is my model
@@ -48,6 +55,11 @@ var AssetStore3D = class {
48
55
  */
49
56
  errors() {
50
57
  const out = [];
58
+ for (const { url, error } of this.textures.errors()) out.push({
59
+ ref: url,
60
+ url,
61
+ error
62
+ });
51
63
  for (const [ref, url] of this.modelKeys) {
52
64
  const entry = this.models.get(url);
53
65
  if (entry?.error) out.push({
@@ -144,6 +156,7 @@ var AssetStore3D = class {
144
156
  return entry;
145
157
  }
146
158
  dispose() {
159
+ this.textures.dispose();
147
160
  for (const entry of this.models.values()) entry.scene?.traverse((obj) => {
148
161
  const mesh = obj;
149
162
  mesh.geometry?.dispose();
@@ -1419,7 +1432,7 @@ function walk(node, parentObj, state) {
1419
1432
  return;
1420
1433
  }
1421
1434
  if (obj.parent !== parentObj) parentObj.add(obj);
1422
- node._syncObject3D(state.alpha);
1435
+ node._syncObject3D(state.alpha, state.assets ?? null);
1423
1436
  if (state.assets && node instanceof ModelInstance3D) node._syncModel(state.assets);
1424
1437
  if (typeof node._onRender3D === "function") state.renderHooks.push(node);
1425
1438
  if (state.sunDirection) {
@@ -2263,6 +2276,7 @@ async function createGame3D(opts) {
2263
2276
  });
2264
2277
  } catch {}
2265
2278
  cleanups.push(engine.settings.bindAudio(engine.audio));
2279
+ cleanups.push(engine.settings.bindLocale(engine.locale));
2266
2280
  await report(1, "ready");
2267
2281
  function disposeGame() {
2268
2282
  teardown([
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RendererStats, lt as LogLevel, v as Engine } from "./behavior-CtPScDMo.js";
1
+ import { T as RendererStats, b as Engine, wt as LogLevel } from "./behavior-CGSCWOHB.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-BwR0DxeM.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-Mig5fY4n.js";
2
2
  //#region src/core/order-groups.ts
3
3
  const ORDER_GROUP_BASE = {
4
4
  background: -2e3,
@@ -1,4 +1,4 @@
1
- import { s as JsonObject } from "./schema-CcoWb32N.js";
1
+ import { s as JsonObject } from "./schema-3ywbdlrv.js";
2
2
 
3
3
  //#region src/core/diagnostics.d.ts
4
4
  /** The minimum an engine has to look like to receive a diagnostic. */