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
package/dist/env.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Rng, i as SceneJson, n as NodeJson, s as JsonObject } from "./schema-CcoWb32N.js";
1
+ import { a as Rng, i as SceneJson, n as NodeJson, s as JsonObject } from "./schema-3ywbdlrv.js";
2
2
 
3
3
  //#region src/env/arena.d.ts
4
4
  /** Valid `generateArena` themes — drives the catalog options AND validation. */
@@ -1,13 +1,157 @@
1
- import { t as registerCoreNodes } from "./register-G3edsjJ2.js";
1
+ import { t as registerCoreNodes } from "./register-BFLg0-_i.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 { i as getNodeSchema, l as registerNode } from "./registry-CJdGpT2V.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
7
- import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-BxFtmfSe.js";
7
+ import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-BvhcQbfJ.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
- import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
9
+ import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
11
+ //#region src/3d/textures.ts
12
+ /**
13
+ * One decode per image, however many nodes want it.
14
+ *
15
+ * Four places each built their own `new TextureLoader()` per node — `Sprite3D`,
16
+ * `AnimatedSprite3D`, `MeshInstance3D`, `Terrain3D` — so a hundred sprite mobs
17
+ * sharing one atlas meant a hundred fetches, a hundred decodes and a hundred GPU
18
+ * uploads. `AssetStore3D` knew only `model` and `animation`; textures had no
19
+ * owner at all. The 2D side has solved this since day one (`2d/assets.ts`).
20
+ *
21
+ * `tree-3d.ts` keeps its own map and is deliberately NOT folded in here: it
22
+ * already dedupes, it decodes off-thread through `ImageBitmapLoader`, and its
23
+ * page-session lifetime is load-bearing (the editor builds and disposes three
24
+ * renderers, so store-scoping it would re-decode every bark and leaf on every
25
+ * Play). Nothing to fix there.
26
+ *
27
+ * **`acquire` returns the SHARED texture.** That is the important default and it
28
+ * is deliberately not a clone: `Texture.copy()` ends with `needsUpdate = true`,
29
+ * which bumps `source.version`, which makes the next draw re-upload the whole
30
+ * image. Cloning on every acquire would turn "spawn an enemy per frame" into
31
+ * "re-upload the atlas per frame" — precisely the stall this file exists to
32
+ * remove.
33
+ *
34
+ * `acquireOwn` is for the two nodes that genuinely hold per-node UV state:
35
+ * `AnimatedSprite3D` (a frame window) and `MeshInstance3D` (`repeat`). A clone
36
+ * shares `texture.source`, so three still fetches and decodes once and, when the
37
+ * sampler parameters match, hands out one GL texture for all of them.
38
+ */
39
+ const defaultLoad = (url, onError) => new TextureLoader().load(url, void 0, void 0, onError);
40
+ function keyOf(url, spec) {
41
+ return [
42
+ url,
43
+ spec?.colorSpace ?? "srgb",
44
+ spec?.filter ?? "linear",
45
+ spec?.wrap ?? "clamp",
46
+ String(spec?.anisotropy ?? 1),
47
+ spec?.process?.id ?? ""
48
+ ].join("|");
49
+ }
50
+ function dress(texture, spec) {
51
+ texture.colorSpace = (spec?.colorSpace ?? "srgb") === "srgb" ? SRGBColorSpace : NoColorSpace;
52
+ const nearest = spec?.filter === "nearest";
53
+ texture.magFilter = nearest ? NearestFilter : LinearFilter;
54
+ texture.generateMipmaps = !nearest;
55
+ texture.minFilter = nearest ? NearestFilter : LinearMipmapLinearFilter;
56
+ const wrap = spec?.wrap === "repeat" ? RepeatWrapping : ClampToEdgeWrapping;
57
+ texture.wrapS = wrap;
58
+ texture.wrapT = wrap;
59
+ if (spec?.anisotropy) texture.anisotropy = spec.anisotropy;
60
+ }
61
+ var TextureCache3D = class {
62
+ entries = /* @__PURE__ */ new Map();
63
+ load;
64
+ diagnostics;
65
+ constructor(opts) {
66
+ this.load = opts?.loader ?? defaultLoad;
67
+ this.diagnostics = opts?.diagnostics ?? null;
68
+ }
69
+ /**
70
+ * The SHARED texture for this url+spec, or null with no DOM to decode in.
71
+ *
72
+ * Callers must NOT mutate `offset`/`repeat`/`center`/`rotation` on it — that
73
+ * is what `acquireOwn` is for — and must not dispose it; the store owns it.
74
+ */
75
+ acquire(url, spec) {
76
+ if (url === "") return null;
77
+ if (typeof document === "undefined" && this.load === defaultLoad) return null;
78
+ const key = keyOf(url, spec);
79
+ const found = this.entries.get(key);
80
+ if (found) return found.texture;
81
+ const entry = {
82
+ url,
83
+ texture: null,
84
+ error: null
85
+ };
86
+ entry.texture = this.load(url, (error) => {
87
+ entry.error = error instanceof Error ? error.message : String(error);
88
+ this.diagnostics?.warn(`texture failed to load: ${url} — ${entry.error}`);
89
+ });
90
+ dress(entry.texture, spec);
91
+ spec?.process?.run(entry.texture);
92
+ this.entries.set(key, entry);
93
+ return entry.texture;
94
+ }
95
+ /**
96
+ * A private CLONE, for a node that writes its own UV window.
97
+ *
98
+ * The caller owns it and disposes it in `free()`. That is safe: three
99
+ * refcounts the GPU texture per `source`, so disposing one clone does not
100
+ * pull the image out from under the others.
101
+ */
102
+ acquireOwn(url, spec) {
103
+ const master = this.acquire(url, spec);
104
+ return master ? master.clone() : null;
105
+ }
106
+ /**
107
+ * What 404'd, by URL — reported raw, never by cache key, because an agent
108
+ * greps this against the URL it wrote.
109
+ */
110
+ errors() {
111
+ const out = [];
112
+ for (const entry of this.entries.values()) if (entry.error !== null) out.push({
113
+ url: entry.url,
114
+ error: entry.error
115
+ });
116
+ return out;
117
+ }
118
+ /** How many distinct images this store has decoded — the point, measurable. */
119
+ size() {
120
+ return this.entries.size;
121
+ }
122
+ dispose() {
123
+ for (const entry of this.entries.values()) entry.texture.dispose();
124
+ this.entries.clear();
125
+ }
126
+ };
127
+ /**
128
+ * The one call a node makes: shared texture through the store, or an uncached
129
+ * load when there is no store (a detached probe, a headless test).
130
+ *
131
+ * The fallback is deliberately silent and deliberately uncached — a node that
132
+ * renders outside a renderer walk is a legitimate case (`Tree3D._trunk()` builds
133
+ * detached), and failing there would be worse than doing the work twice.
134
+ */
135
+ function acquireTexture(assets, url, spec) {
136
+ if (assets) return assets.textures.acquire(url, spec);
137
+ return uncached().acquire(url, spec);
138
+ }
139
+ /** As `acquireTexture`, but a private clone for a node with its own UV window. */
140
+ function acquireOwnTexture(assets, url, spec) {
141
+ if (assets) return assets.textures.acquireOwn(url, spec);
142
+ return uncached().acquireOwn(url, spec);
143
+ }
144
+ /**
145
+ * A throwaway cache for the store-less path.
146
+ *
147
+ * Not a module-level shared instance: that is the module-level engine state the
148
+ * repo bans, and it is what `tree-3d.ts` had. One decode per call here is the
149
+ * honest cost of rendering outside a renderer.
150
+ */
151
+ function uncached() {
152
+ return new TextureCache3D();
153
+ }
154
+ //#endregion
11
155
  //#region src/3d/camera-rig.ts
12
156
  const QUARTER_PITCH = Math.atan(1 / Math.SQRT2);
13
157
  function rigPose(view, player, eyeHeight, yaw, pitch, distance) {
@@ -227,8 +371,8 @@ var MeshInstance3D = class extends Node3D {
227
371
  _createObject3D() {
228
372
  return new Mesh(new BoxGeometry(1, 1, 1), new MeshPhysicalMaterial());
229
373
  }
230
- _syncObject3D() {
231
- super._syncObject3D();
374
+ _syncObject3D(alpha = 1, assets = null) {
375
+ super._syncObject3D(alpha, assets);
232
376
  const m = this._ensureObject3D();
233
377
  const key = `${this.mesh}:${JSON.stringify(this.size)}`;
234
378
  if (key !== this.geometryKey) {
@@ -238,14 +382,14 @@ var MeshInstance3D = class extends Node3D {
238
382
  }
239
383
  const mat = m.material;
240
384
  applyMeshMaterial(mat, this.material);
241
- this.syncTextures(mat);
385
+ this.syncTextures(mat, assets);
242
386
  m.castShadow = this.castShadow;
243
387
  m.receiveShadow = this.receiveShadow;
244
388
  }
245
389
  /** Lazy texture dressing — same headless guard as Tree3D bark: without a
246
390
  * DOM there is no Image decode, so geometry/colors apply and the maps are
247
391
  * skipped. Re-loads only when a texture-shaping key changes. */
248
- syncTextures(mat) {
392
+ syncTextures(mat, assets) {
249
393
  const key = JSON.stringify([
250
394
  this.material.map ?? "",
251
395
  this.material.normalMap ?? "",
@@ -258,25 +402,50 @@ var MeshInstance3D = class extends Node3D {
258
402
  const hadMaps = mat.map !== null || mat.normalMap !== null;
259
403
  mat.map = null;
260
404
  mat.normalMap = null;
261
- const loader = typeof document !== "undefined" ? new TextureLoader() : null;
262
- if (loader && this.material.map) mat.map = this.loadTexture(loader, this.material.map, true);
263
- if (loader && this.material.normalMap) mat.normalMap = this.loadTexture(loader, this.material.normalMap, false);
405
+ if (this.material.map) mat.map = this.loadTexture(assets, this.material.map, true);
406
+ if (this.material.normalMap) mat.normalMap = this.loadTexture(assets, this.material.normalMap, false);
264
407
  if (hadMaps || mat.map || mat.normalMap) mat.needsUpdate = true;
265
408
  }
266
- loadTexture(loader, url, srgb) {
267
- const texture = loader.load(url);
268
- texture.wrapS = RepeatWrapping;
269
- texture.wrapT = RepeatWrapping;
409
+ /**
410
+ * A private CLONE: `repeat` is per-node here (a 1x1 crate and a 8x8 floor
411
+ * share one tile image), and that is the one field a shared master cannot
412
+ * carry. The clone shares `texture.source`, so the fetch and the decode are
413
+ * still done once per url for the whole scene.
414
+ */
415
+ loadTexture(assets, url, srgb) {
416
+ const texture = acquireOwnTexture(assets, url, {
417
+ wrap: "repeat",
418
+ anisotropy: 4,
419
+ colorSpace: srgb ? "srgb" : "linear"
420
+ });
421
+ if (!texture) return null;
270
422
  const [u, v] = this.material.repeat ?? [1, 1];
271
423
  texture.repeat.set(u, v);
272
- texture.anisotropy = 4;
273
- if (srgb) texture.colorSpace = SRGBColorSpace;
274
424
  this.loadedTextures.push(texture);
275
425
  return texture;
276
426
  }
427
+ /**
428
+ * Give the GPU back everything this node OWNS.
429
+ *
430
+ * It used to dispose textures and nothing else, so every freed mesh leaked its
431
+ * geometry and its material — a wave spawner that kills and respawns fifty
432
+ * enemies a minute leaks fifty of each, forever, and nothing in the game says
433
+ * so. Both are built here (`_createObject3D` news them, `_syncObject3D`
434
+ * rebuilds the geometry on a shape change), so both are ours to release.
435
+ *
436
+ * The textures are NOT: since the shared texture cache they belong to the
437
+ * asset store, except the per-node clones this node asked for, which are what
438
+ * `loadedTextures` holds.
439
+ */
277
440
  free() {
278
441
  for (const texture of this.loadedTextures) texture.dispose();
279
442
  this.loadedTextures = [];
443
+ const mesh = this._builtObject3D;
444
+ if (mesh) {
445
+ mesh.geometry?.dispose();
446
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
447
+ for (const material of materials) material?.dispose();
448
+ }
280
449
  super.free();
281
450
  }
282
451
  };
@@ -535,7 +704,10 @@ var Joint3D = class extends Node3D {
535
704
  "spring"
536
705
  ]
537
706
  },
538
- target: { default: "" },
707
+ target: {
708
+ default: "",
709
+ nodePath: true
710
+ },
539
711
  anchor: { default: [
540
712
  0,
541
713
  0,
@@ -1417,7 +1589,6 @@ var Terrain3D = class extends Node3D {
1417
1589
  heightmap = null;
1418
1590
  heightmapKey = "";
1419
1591
  builtKey = "";
1420
- loadedTextures = [];
1421
1592
  _createObject3D() {
1422
1593
  const geometry = new BufferGeometry();
1423
1594
  geometry.setAttribute("position", new BufferAttribute(new Float32Array(0), 3));
@@ -1516,9 +1687,9 @@ var Terrain3D = class extends Node3D {
1516
1687
  if (this.theme === "custom") return this.layers;
1517
1688
  return terrainThemeLayers(this.theme, this.textureBase);
1518
1689
  }
1519
- _syncObject3D() {
1520
- super._syncObject3D();
1521
- this.rebuildIfNeeded();
1690
+ _syncObject3D(alpha = 1, assets = null) {
1691
+ super._syncObject3D(alpha, assets);
1692
+ this.rebuildIfNeeded(assets);
1522
1693
  }
1523
1694
  effectiveIslandEdge() {
1524
1695
  return this.islandEdge ?? this.theme === "island";
@@ -1527,7 +1698,7 @@ var Terrain3D = class extends Node3D {
1527
1698
  return Math.max(2, Math.min(Math.floor(this.resolution), MAX_RESOLUTION));
1528
1699
  }
1529
1700
  /** Rebuild geometry+material only when a terrain-shaping prop changed. */
1530
- rebuildIfNeeded() {
1701
+ rebuildIfNeeded(assets) {
1531
1702
  const key = JSON.stringify([
1532
1703
  this.size,
1533
1704
  this.maxHeight,
@@ -1549,8 +1720,7 @@ var Terrain3D = class extends Node3D {
1549
1720
  mesh.geometry.dispose();
1550
1721
  mesh.geometry = this.buildGeometry();
1551
1722
  mesh.material.dispose();
1552
- this.disposeTextures();
1553
- mesh.material = this.buildMaterial();
1723
+ mesh.material = this.buildMaterial(assets);
1554
1724
  mesh.receiveShadow = true;
1555
1725
  mesh.castShadow = true;
1556
1726
  }
@@ -1583,20 +1753,14 @@ var Terrain3D = class extends Node3D {
1583
1753
  geometry.setAttribute("color", new BufferAttribute(colors, 3));
1584
1754
  return geometry;
1585
1755
  }
1586
- buildMaterial() {
1756
+ buildMaterial(assets) {
1587
1757
  const layers = this.resolvedLayers();
1588
1758
  const material = new MeshStandardMaterial({ vertexColors: true });
1589
- const loader = typeof document !== "undefined" ? new TextureLoader() : null;
1590
- const load = (url, srgb) => {
1591
- if (!loader || !url) return null;
1592
- const tex = loader.load(url);
1593
- tex.wrapS = RepeatWrapping;
1594
- tex.wrapT = RepeatWrapping;
1595
- tex.anisotropy = 8;
1596
- if (srgb) tex.colorSpace = SRGBColorSpace;
1597
- this.loadedTextures.push(tex);
1598
- return tex;
1599
- };
1759
+ const load = (url, srgb) => url ? acquireTexture(assets, url, {
1760
+ wrap: "repeat",
1761
+ anisotropy: 8,
1762
+ colorSpace: srgb ? "srgb" : "linear"
1763
+ }) : null;
1600
1764
  const textures = layers.map((l) => load(l.texture, true));
1601
1765
  const normalMaps = layers.map((l) => load(l.normalMap, false));
1602
1766
  const hasNormalMaps = normalMaps.some((t) => t !== null);
@@ -1621,15 +1785,10 @@ var Terrain3D = class extends Node3D {
1621
1785
  material.customProgramCacheKey = () => `incanto-terrain-splat:${hasNormalMaps ? 1 : 0}`;
1622
1786
  return material;
1623
1787
  }
1624
- disposeTextures() {
1625
- for (const tex of this.loadedTextures) tex.dispose();
1626
- this.loadedTextures = [];
1627
- }
1628
1788
  free() {
1629
1789
  const mesh = this._ensureObject3D();
1630
1790
  mesh.geometry.dispose();
1631
1791
  mesh.material.dispose();
1632
- this.disposeTextures();
1633
1792
  this.heightmap = null;
1634
1793
  super.free();
1635
1794
  }
@@ -1776,25 +1935,16 @@ var Sprite3D = class extends Node3D {
1776
1935
  return this.quadMesh;
1777
1936
  }
1778
1937
  /** Override point: AnimatedSprite3D substitutes its frame window here. */
1779
- resolveTexture() {
1938
+ resolveTexture(assets) {
1780
1939
  if (!this.texture) return null;
1781
1940
  if (this.textureRef !== this.texture) {
1782
- this.loadedTexture?.dispose();
1783
- this.loadedTexture = null;
1784
1941
  this.textureRef = this.texture;
1785
- if (typeof document !== "undefined") {
1786
- const tex = new TextureLoader().load(this.texture);
1787
- tex.colorSpace = SRGBColorSpace;
1788
- const filter = this.pixelArt ? NearestFilter : LinearFilter;
1789
- tex.magFilter = filter;
1790
- tex.minFilter = filter;
1791
- this.loadedTexture = tex;
1792
- }
1942
+ this.loadedTexture = acquireTexture(assets, this.texture, { filter: this.pixelArt ? "nearest" : "linear" });
1793
1943
  }
1794
1944
  return this.loadedTexture;
1795
1945
  }
1796
- _syncObject3D(alpha = 1) {
1797
- super._syncObject3D(alpha);
1946
+ _syncObject3D(alpha = 1, assets = null) {
1947
+ super._syncObject3D(alpha, assets);
1798
1948
  const quad = this._quad();
1799
1949
  const mat = quad.material;
1800
1950
  const w = this.size[0] ?? 1;
@@ -1803,7 +1953,7 @@ var Sprite3D = class extends Node3D {
1803
1953
  const ax = this.anchor[0] ?? .5;
1804
1954
  const ay = this.anchor[1] ?? 0;
1805
1955
  quad.position.set((.5 - ax) * w, (.5 - ay) * h, 0);
1806
- const tex = this.resolveTexture();
1956
+ const tex = this.resolveTexture(assets);
1807
1957
  if (mat.map !== tex) {
1808
1958
  mat.map = tex;
1809
1959
  mat.needsUpdate = true;
@@ -1999,20 +2149,12 @@ var AnimatedSprite3D = class extends Sprite3D {
1999
2149
  }
2000
2150
  this.currentFrame = this.frameList[this.frameIndex] ?? 0;
2001
2151
  }
2002
- resolveTexture() {
2152
+ resolveTexture(assets) {
2003
2153
  if (!this.sheet || this.frameWidth <= 0 || this.frameHeight <= 0) return null;
2004
2154
  if (this.loadedSheetRef !== this.sheet) {
2005
2155
  this.sheetTexture?.dispose();
2006
- this.sheetTexture = null;
2007
2156
  this.loadedSheetRef = this.sheet;
2008
- if (typeof document !== "undefined") {
2009
- const tex = new TextureLoader().load(this.sheet);
2010
- tex.colorSpace = SRGBColorSpace;
2011
- const filter = this.pixelArt ? NearestFilter : LinearFilter;
2012
- tex.magFilter = filter;
2013
- tex.minFilter = filter;
2014
- this.sheetTexture = tex;
2015
- }
2157
+ this.sheetTexture = acquireOwnTexture(assets, this.sheet, { filter: this.pixelArt ? "nearest" : "linear" });
2016
2158
  }
2017
2159
  const tex = this.sheetTexture;
2018
2160
  if (!tex) return null;
@@ -2141,7 +2283,10 @@ var BoneAttachment3D = class extends Node3D {
2141
2283
  static typeName = "BoneAttachment3D";
2142
2284
  static props = {
2143
2285
  /** Node path to the ModelInstance3D whose skeleton to ride. */
2144
- target: { default: "" },
2286
+ target: {
2287
+ default: "",
2288
+ nodePath: true
2289
+ },
2145
2290
  /** Bone name ("RightHand", "Head", …). Empty = inert. */
2146
2291
  bone: { default: "" }
2147
2292
  };
@@ -2205,11 +2350,17 @@ var BoneLookAt3D = class extends Node3D {
2205
2350
  static typeName = "BoneLookAt3D";
2206
2351
  static props = {
2207
2352
  /** Node path to the ModelInstance3D whose bone to steer. */
2208
- target: { default: "" },
2353
+ target: {
2354
+ default: "",
2355
+ nodePath: true
2356
+ },
2209
2357
  /** Bone name ("Head", "Neck", "Spine2" — Mixamo spellings matched). */
2210
2358
  bone: { default: "Head" },
2211
2359
  /** Node path to LOOK AT ('' = idle). */
2212
- lookAt: { default: "" },
2360
+ lookAt: {
2361
+ default: "",
2362
+ nodePath: true
2363
+ },
2213
2364
  /** Comfort cone half-angle, degrees — beyond it the head returns to the anim. */
2214
2365
  maxAngleDeg: { default: 75 },
2215
2366
  /** Blend weight 0..1 (how far toward the target the bone turns). */
@@ -2400,7 +2551,10 @@ var CharacterController3D = class extends Node3D {
2400
2551
  moveAction: { default: "move" },
2401
2552
  jumpAction: { default: "jump" },
2402
2553
  sprintAction: { default: "sprint" },
2403
- skinPath: { default: "../Skin" },
2554
+ skinPath: {
2555
+ default: "../Skin",
2556
+ nodePath: true
2557
+ },
2404
2558
  skinYawOffset: { default: 0 },
2405
2559
  animations: { default: {} }
2406
2560
  };
@@ -3319,7 +3473,10 @@ var Flowers3D = class extends Node3D {
3319
3473
  sway: { default: .5 },
3320
3474
  drape: { default: null },
3321
3475
  avoidWater: { default: true },
3322
- terrain: { default: "" }
3476
+ terrain: {
3477
+ default: "",
3478
+ nodePath: true
3479
+ }
3323
3480
  };
3324
3481
  /** 'lush' | 'sparse' | 'none' | plants per m² (load-time check + budget). */
3325
3482
  density = "sparse";
@@ -4720,7 +4877,10 @@ var Foliage3D = class Foliage3D extends Node3D {
4720
4877
  seed: { default: 1 },
4721
4878
  drape: { default: null },
4722
4879
  avoidWater: { default: true },
4723
- terrain: { default: "" },
4880
+ terrain: {
4881
+ default: "",
4882
+ nodePath: true
4883
+ },
4724
4884
  renderOrder: { default: 2 }
4725
4885
  };
4726
4886
  /** Transparent blades draw after water (1) + opaque ground (overrides Node3D's 0). */
@@ -7984,7 +8144,10 @@ var River3D = class River3D extends Node3D {
7984
8144
  ] },
7985
8145
  sunColor: { default: "#fff6e0" },
7986
8146
  sunIntensity: { default: 1 },
7987
- terrain: { default: "" },
8147
+ terrain: {
8148
+ default: "",
8149
+ nodePath: true
8150
+ },
7988
8151
  carve: { default: true },
7989
8152
  flowForce: { default: 1 },
7990
8153
  spray: { default: 1 }
@@ -10394,7 +10557,10 @@ var Tree3D = class Tree3D extends Node3D {
10394
10557
  leafFadeEnd: { default: 0 },
10395
10558
  leafShadows: { default: true },
10396
10559
  drape: { default: null },
10397
- terrain: { default: "" }
10560
+ terrain: {
10561
+ default: "",
10562
+ nodePath: true
10563
+ }
10398
10564
  };
10399
10565
  /** 'simple' (primitive low-poly) | 'medium' (light ez-trees) | 'high' (full). */
10400
10566
  tier = "medium";
@@ -11909,4 +12075,4 @@ function resolveEnvironmentHdri(env) {
11909
12075
  return null;
11910
12076
  }
11911
12077
  //#endregion
11912
- export { Terrain3D as A, keyboardIntensity as B, resolveFlowerDensity as C, BoneLookAt3D as D, Camera3D as E, InstancedMesh3D as F, rigPose as H, MeshInstance3D as I, buildMeshGeometry as L, TERRAIN_THEMES as M, terrainThemeLayers as N, BoneAttachment3D as O, Joint3D as P, QUARTER_PITCH as R, Flowers3D as S, CharacterController3D as T, movementState as V, LoftMesh3D as _, Tree3D as a, Foliage3D as b, buildRiverRings as c, riverCarveChannels as d, riverStepFor as f, ModelInstance3D as g, Particles3D as h, VoxelGrid3D as i, DEFAULT_TERRAIN_TEXTURE_BASE as j, Billboard3D as k, findRiverCoverageGaps as l, traceDownhillPath as m, registerNodes3D as n, Trail3D as o, smoothCourse as p, VOXEL_PALETTE as r, River3D as s, resolveEnvironmentHdri as t, projectToRiver as u, DirectionalLight3D as v, FLOWER_VARIETIES as w, DENSITY_PRESETS as x, OmniLight3D as y, cameraRelative as z };
12078
+ export { Terrain3D as A, keyboardIntensity as B, resolveFlowerDensity as C, BoneLookAt3D as D, Camera3D as E, InstancedMesh3D as F, acquireTexture as G, rigPose as H, MeshInstance3D as I, buildMeshGeometry as L, TERRAIN_THEMES as M, terrainThemeLayers as N, BoneAttachment3D as O, Joint3D as P, QUARTER_PITCH as R, Flowers3D as S, CharacterController3D as T, TextureCache3D as U, movementState as V, acquireOwnTexture as W, LoftMesh3D as _, Tree3D as a, Foliage3D as b, buildRiverRings as c, riverCarveChannels as d, riverStepFor as f, ModelInstance3D as g, Particles3D as h, VoxelGrid3D as i, DEFAULT_TERRAIN_TEXTURE_BASE as j, Billboard3D as k, findRiverCoverageGaps as l, traceDownhillPath as m, registerNodes3D as n, Trail3D as o, smoothCourse as p, VOXEL_PALETTE as r, River3D as s, resolveEnvironmentHdri as t, projectToRiver as u, DirectionalLight3D as v, FLOWER_VARIETIES as w, DENSITY_PRESETS as x, OmniLight3D as y, cameraRelative as z };
@@ -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/audit.d.ts
4
4
  /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
@@ -1,7 +1,7 @@
1
- import { f as Node, g as Behavior, n as loadScene, y as registerBehavior } from "./loader-BwR0DxeM.js";
1
+ import { f as Node, g as Behavior, n as loadScene, x as registerBehavior } from "./loader-Mig5fY4n.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
4
- import { r as effectiveOrder, t as duplicateNode } from "./duplicate-DlOvknDO.js";
4
+ import { r as effectiveOrder, t as duplicateNode } from "./duplicate-C716f-97.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -2390,8 +2390,15 @@ var Node3D = class Node3D extends Node {
2390
2390
  }
2391
2391
  /** @internal Push JSON props onto the backing object. Called every frame.
2392
2392
  * `alpha` (0..1) interpolates a physics body between its last two fixed-step
2393
- * positions; non-physics nodes ignore it and render `position` directly. */
2394
- _syncObject3D(alpha = 1) {
2393
+ * positions; non-physics nodes ignore it and render `position` directly.
2394
+ *
2395
+ * `assets` is the renderer's store, the only route a node has to shared
2396
+ * textures — mirroring `_syncObject2D(assets)`, which has always taken it. It
2397
+ * is a PARAMETER rather than a field so a node that forgets it is a type
2398
+ * error rather than a silent per-node decode; overrides that do not need it
2399
+ * simply keep the one-argument signature. Null in a headless test or a
2400
+ * detached probe, where the node falls back to loading uncached. */
2401
+ _syncObject3D(alpha = 1, _assets = null) {
2395
2402
  const o = this._ensureObject3D();
2396
2403
  const prev = this._interpPrev;
2397
2404
  const curr = this._interpCurr;
@@ -3973,7 +3980,10 @@ const MIN_DRAFT_REF = .15;
3973
3980
  var Buoyancy = class extends Behavior {
3974
3981
  static props = {
3975
3982
  /** Water node path; empty = the first Water3D in the tree. */
3976
- water: { default: "" },
3983
+ water: {
3984
+ default: "",
3985
+ nodePath: true
3986
+ },
3977
3987
  /** Where the resting WATERLINE sits below the body origin, in meters. A
3978
3988
  * hull whose origin is its interior floor wants a draft of ~0.1: the water
3979
3989
  * then rests that far under the floor and the boat is dry inside. */
@@ -4191,7 +4201,10 @@ function smoothingFactor(smoothing, dt) {
4191
4201
  var Chase = class extends Behavior {
4192
4202
  static props = {
4193
4203
  /** NodePath of the node to chase (resolved from THIS behavior's node). */
4194
- target: { default: "" },
4204
+ target: {
4205
+ default: "",
4206
+ nodePath: true
4207
+ },
4195
4208
  /** Units per second toward the target. */
4196
4209
  speed: { default: 60 },
4197
4210
  /** Stop (and emit reachedTarget) when within this distance. */
@@ -4362,6 +4375,14 @@ group: { default: "player" } };
4362
4375
  this.total += value;
4363
4376
  this.emit("totalChanged", this.total);
4364
4377
  }
4378
+ /** How many have been picked up so far. */
4379
+ serialize() {
4380
+ return { total: this.total };
4381
+ }
4382
+ deserialize(data) {
4383
+ const d = data;
4384
+ if (typeof d.total === "number") this.total = d.total;
4385
+ }
4365
4386
  };
4366
4387
  //#endregion
4367
4388
  //#region src/gameplay/health.ts
@@ -4455,6 +4476,18 @@ var Health = class extends Behavior {
4455
4476
  this.emit("died");
4456
4477
  if (this.freeOnDeath) this.node.queueFree();
4457
4478
  }
4479
+ /** Current HP and whether death already fired — `max` comes back from JSON. */
4480
+ serialize() {
4481
+ return {
4482
+ current: this.current,
4483
+ dead: this.dead
4484
+ };
4485
+ }
4486
+ deserialize(data) {
4487
+ const d = data;
4488
+ if (typeof d.current === "number") this.current = Math.min(this.max, Math.max(0, d.current));
4489
+ if (typeof d.dead === "boolean") this.dead = d.dead;
4490
+ }
4458
4491
  };
4459
4492
  //#endregion
4460
4493
  //#region src/gameplay/trigger.ts
@@ -4686,7 +4719,10 @@ function phaseOf(hour) {
4686
4719
  var FollowCamera = class extends Behavior {
4687
4720
  static props = {
4688
4721
  /** NodePath of the node to follow (relative to this node, or absolute). */
4689
- target: { default: "" },
4722
+ target: {
4723
+ default: "",
4724
+ nodePath: true
4725
+ },
4690
4726
  /** Constant offset added to the target's position ([x,y] or [x,y,z]). */
4691
4727
  offset: { default: [] },
4692
4728
  /** 0..1 retention factor: 0 = snap, higher = smoother chase. */
@@ -4802,7 +4838,10 @@ var GameFlow = class extends Behavior {
4802
4838
  static props = {
4803
4839
  restartAction: { default: "restart" },
4804
4840
  freezeOnEnd: { default: true },
4805
- bannerPath: { default: "%Banner" }
4841
+ bannerPath: {
4842
+ default: "%Banner",
4843
+ nodePath: true
4844
+ }
4806
4845
  };
4807
4846
  /** Input action that restarts from gameover/won (declare it in `input{}`). */
4808
4847
  restartAction = "restart";
@@ -5603,6 +5642,22 @@ var ScoreKeeper = class extends Behavior {
5603
5642
  this.emit("lost");
5604
5643
  }
5605
5644
  }
5645
+ /** The run's progress. `scoreToWin` is balance and comes back from JSON. */
5646
+ serialize() {
5647
+ return {
5648
+ score: this.score,
5649
+ lives: this.lives,
5650
+ hasWon: this.hasWon,
5651
+ hasLost: this.hasLost
5652
+ };
5653
+ }
5654
+ deserialize(data) {
5655
+ const d = data;
5656
+ if (typeof d.score === "number") this.score = d.score;
5657
+ if (typeof d.lives === "number") this.lives = d.lives;
5658
+ if (typeof d.hasWon === "boolean") this.hasWon = d.hasWon;
5659
+ if (typeof d.hasLost === "boolean") this.hasLost = d.hasLost;
5660
+ }
5606
5661
  };
5607
5662
  //#endregion
5608
5663
  //#region src/gameplay/spawn-source.ts
@@ -5686,7 +5741,10 @@ var SpawnSource = class {
5686
5741
  var Spawner = class extends Behavior {
5687
5742
  static props = {
5688
5743
  /** Node path of the template to clone. */
5689
- prefab: { default: "" },
5744
+ prefab: {
5745
+ default: "",
5746
+ nodePath: true
5747
+ },
5690
5748
  /** Seconds between spawns. */
5691
5749
  interval: { default: 1 },
5692
5750
  /** Max LIVE instances (0 = unlimited). */
@@ -5964,7 +6022,10 @@ var WaveSpawner = class extends Behavior {
5964
6022
  var ZombieAI = class extends Behavior {
5965
6023
  static props = {
5966
6024
  /** NodePath of the node to charge once near (usually the player). */
5967
- aggroTarget: { default: "" },
6025
+ aggroTarget: {
6026
+ default: "",
6027
+ nodePath: true
6028
+ },
5968
6029
  /** Start charging when the target is within this distance. */
5969
6030
  aggroRange: { default: 12 },
5970
6031
  /** Give up the charge past this distance (0 = aggroRange × 1.5). */
@@ -5978,7 +6039,10 @@ var ZombieAI = class extends Behavior {
5978
6039
  /** Seconds before forcibly choosing a new roam goal. */
5979
6040
  wanderChangeEvery: { default: 3 },
5980
6041
  /** Optional NodePath to DRIFT toward while roaming (e.g. the objective). */
5981
- goalTarget: { default: "" },
6042
+ goalTarget: {
6043
+ default: "",
6044
+ nodePath: true
6045
+ },
5982
6046
  /** Stop (and idle) this close to the active target. */
5983
6047
  stopRange: { default: 1.2 },
5984
6048
  /** Move the parent node instead of this one (AI-on-a-child pattern). */