incanto 0.70.0 → 0.72.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 (74) hide show
  1. package/bin/incanto-editor.mjs +15 -9
  2. package/bin/incanto-feel.mjs +11 -1
  3. package/bin/incanto-play.mjs +11 -1
  4. package/bin/incanto-playtest.mjs +11 -1
  5. package/bin/incanto-verify.mjs +36 -98
  6. package/dist/2d.d.ts +18 -3
  7. package/dist/2d.js +3 -3
  8. package/dist/3d.d.ts +3 -3
  9. package/dist/3d.js +6 -6
  10. package/dist/{agent8-CmNF01gA.js → agent8-BqZ0_O1u.js} +1 -1
  11. package/dist/{audio-player-DaMxqfNE.d.ts → audio-player-ioZ7VKC6.d.ts} +1 -1
  12. package/dist/{behavior-DZExDn9o.d.ts → behavior-Bf9P9oB2.d.ts} +12 -1
  13. package/dist/{create-game-Bwvh6q8A.js → create-game-C9rWqqAC.js} +37 -13
  14. package/dist/{create-game-C7ffQWW7.js → create-game-CmFAtLZp.js} +8 -7
  15. package/dist/debug.d.ts +1 -1
  16. package/dist/editor.js +1083 -1045
  17. package/dist/{environment-presets-D6Q5BxeE.js → environment-presets-BeHoaGhy.js} +3 -3
  18. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  19. package/dist/{gameplay-BfHkuzVb.js → gameplay-BlQe-M07.js} +108 -6
  20. package/dist/gameplay.d.ts +68 -4
  21. package/dist/gameplay.js +2 -2
  22. package/dist/index.d.ts +27 -4
  23. package/dist/index.js +4 -4
  24. package/dist/{loader-Cff09LMm.d.ts → loader-CBfQzB6S.d.ts} +10 -3
  25. package/dist/net.d.ts +2 -2
  26. package/dist/net.js +2 -2
  27. package/dist/{physics-2d-CE0Qvy3V.js → physics-2d-DrIgTlLk.js} +2 -2
  28. package/dist/{physics-3d-CpH-2gn5.js → physics-3d-BzOCw1VS.js} +3 -3
  29. package/dist/{picking-CQJ_PJKh.js → picking-C9McVJrO.js} +3 -3
  30. package/dist/react.d.ts +1 -1
  31. package/dist/react.js +1 -1
  32. package/dist/{register-6DYnKZcy.js → register-5IYfooAN.js} +49 -15
  33. package/dist/{register-Bkk0wSDB.js → register-C8HuRkOf.js} +100 -8
  34. package/dist/{replay-Dmw-PKQu.d.ts → replay-CBEChqXq.d.ts} +2 -2
  35. package/dist/{replay-DjAkAzMq.js → replay-datiAE-b.js} +15 -8
  36. package/dist/{save-slots-BXVg148r.js → save-slots-CEuJPUle.js} +102 -4
  37. package/dist/{split-screen-PL78oVXP.js → split-screen-Bm5DTElG.js} +3 -3
  38. package/dist/{split-screen--k-XpBjr.d.ts → split-screen-DkqLng6q.d.ts} +2 -2
  39. package/dist/{src-Cxfiv1Hg.js → src-DRSsRleD.js} +1 -1
  40. package/dist/{test-iHYVUcDK.js → test-Djuq3Vhq.js} +252 -24
  41. package/dist/test.d.ts +113 -5
  42. package/dist/test.js +3 -3
  43. package/dist/vite.d.ts +8 -1
  44. package/dist/vite.js +23 -6
  45. package/editor/assets/{agent8-D0MS174y.js → agent8-CMKz2cax.js} +1 -1
  46. package/editor/assets/{debug-BnXkKuYu.js → debug-DLrQm-az.js} +1 -1
  47. package/editor/assets/{index-CIu3uc3l.js → index-BEKuxYcE.js} +94 -94
  48. package/editor/index.html +1 -1
  49. package/package.json +1 -1
  50. package/schemas/scene.schema.json +4 -0
  51. package/skills/incanto-behaviors-and-scripts.md +30 -9
  52. package/skills/incanto-building-2d-games.md +26 -2
  53. package/skills/incanto-editor.md +4 -0
  54. package/skills/incanto-gameplay-behaviors.md +63 -1
  55. package/skills/incanto-hud.md +14 -0
  56. package/skills/incanto-node-reference.md +18 -2
  57. package/skills/incanto-playtesting.md +34 -0
  58. package/skills/incanto-save-slots.md +56 -1
  59. package/skills/incanto-scene-json-authoring.md +30 -0
  60. package/skills/incanto-verifying-your-game.md +11 -0
  61. package/templates-app/beacon-isle-3d/package.json +1 -1
  62. package/templates-app/beacon-isle-3d/src/game.scene.json +396 -386
  63. package/templates-app/molehill-2d/package.json +1 -1
  64. package/templates-app/molehill-2d/src/game.scene.json +10 -60
  65. package/templates-app/platformer-2d/package.json +1 -1
  66. package/templates-app/platformer-2d/src/game.scene.json +268 -504
  67. package/templates-app/star-survivor/package.json +1 -1
  68. package/templates-app/star-survivor/src/game.scene.json +23 -33
  69. package/templates-app/tps-3d/package.json +1 -1
  70. package/templates-app/tps-3d/src/game.scene.json +90 -105
  71. package/templates-app/village-quest-3d/package.json +1 -1
  72. package/templates-app/village-quest-3d/src/behaviors.ts +21 -4
  73. package/templates-app/village-quest-3d/src/grove.scene.json +99 -118
  74. package/templates-app/village-quest-3d/src/village.scene.json +449 -245
@@ -1,9 +1,9 @@
1
- import { K as Node, M as effectiveOrder, q as diagnose } from "./save-slots-BXVg148r.js";
2
- import { M as translateOn, t as registerCoreNodes } from "./register-6DYnKZcy.js";
1
+ import { J as Node, P as effectiveOrder, Y as diagnose } from "./save-slots-CEuJPUle.js";
2
+ import { M as translateOn, t as registerCoreNodes } from "./register-5IYfooAN.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
5
5
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CqR2o3SA.js";
6
- import { E as effectRng, X as localFromWorld, Z as worldPosition, w as tolerateUnknownAction } from "./gameplay-BfHkuzVb.js";
6
+ import { D as effectRng, Q as worldPosition, T as tolerateUnknownAction, Z as localFromWorld } from "./gameplay-BlQe-M07.js";
7
7
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
8
8
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
9
9
  //#region src/2d/physics/collider-validate.ts
@@ -1617,6 +1617,15 @@ function mergeSolidRects(grid, solid) {
1617
1617
  //#endregion
1618
1618
  //#region src/2d/nodes/tile-map-2d.ts
1619
1619
  /**
1620
+ * Where colour cells live in the index space, above any atlas anyone will draw.
1621
+ *
1622
+ * A million tiles is 1000×1000 atlas cells; a texture that size does not exist,
1623
+ * and the loader refuses `colors` and `texture` together anyway.
1624
+ */
1625
+ const COLOR_BASE = 1e6;
1626
+ /** `#rgb` or `#rrggbb`, which is what every other colour prop in the engine takes. */
1627
+ const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
1628
+ /**
1620
1629
  * A whole tile level as ONE node — grid render in a single draw call plus
1621
1630
  * greedy-merged static colliders. Author the level as rows of characters:
1622
1631
  *
@@ -1653,6 +1662,23 @@ var TileMap2D = class extends Node2D {
1653
1662
  cells: { default: [] },
1654
1663
  /** char → atlas tile index for non-digit chars. */
1655
1664
  legend: { default: {} },
1665
+ /**
1666
+ * Char → COLOUR, for a level you author before you have any art.
1667
+ *
1668
+ * `incanto-assets.md` is titled "No art yet? Ship gameplay anyway", and it
1669
+ * was true of everything except the node a grid game is made of: this one
1670
+ * draws from an atlas, so with no texture it rendered nothing at all. A
1671
+ * sokoban built from the published package came out as 24 hand-placed
1672
+ * `ColorRect2D` walls and a `wallAt()` comparing positions — because the
1673
+ * one node that already answers `cellAt`/`tileAt`/`setTile`/`worldAt`, and
1674
+ * merges the colliders, could not be seen.
1675
+ *
1676
+ * A cell painted with one of these chars is a flat quad of that colour, in
1677
+ * the same single draw call. Exclusive with `texture`: one material, one
1678
+ * draw call, and "the art arrived" is a two-line edit rather than a
1679
+ * silently half-textured level.
1680
+ */
1681
+ colors: { default: {} },
1656
1682
  /** Tile indices that get merged static colliders. */
1657
1683
  solid: { default: [] },
1658
1684
  opacity: {
@@ -1693,6 +1719,42 @@ var TileMap2D = class extends Node2D {
1693
1719
  this.geometryDirty = true;
1694
1720
  this.collidersDirty = true;
1695
1721
  }
1722
+ _colors = {};
1723
+ get colors() {
1724
+ return this._colors;
1725
+ }
1726
+ set colors(value) {
1727
+ this._colors = value;
1728
+ this.geometryDirty = true;
1729
+ this.collidersDirty = true;
1730
+ }
1731
+ /**
1732
+ * The colours, in declaration order — a cell's tile index is `COLOR_BASE + i`.
1733
+ *
1734
+ * A synthetic index rather than a second grid: everything downstream (the
1735
+ * collider merge, `tileAt`, `setTile`, the editor's paint tool) already
1736
+ * speaks in indices, and one number per cell keeps it that way.
1737
+ */
1738
+ palette() {
1739
+ return Object.values(this._colors);
1740
+ }
1741
+ /** `legend` plus the colour chars, which is what every cell lookup wants. */
1742
+ allChars() {
1743
+ const merged = { ...this._legend };
1744
+ Object.keys(this._colors).forEach((ch, i) => {
1745
+ merged[ch] = COLOR_BASE + i;
1746
+ });
1747
+ return merged;
1748
+ }
1749
+ /** `solid` as INDICES: an entry may name the char that painted the cell. */
1750
+ solidIndices() {
1751
+ const chars = this.allChars();
1752
+ const out = [];
1753
+ for (const entry of this._solid) if (typeof entry === "number") out.push(entry);
1754
+ else if (chars[entry] !== void 0) out.push(chars[entry]);
1755
+ else if (entry.length === 1 && entry >= "0" && entry <= "9") out.push(entry.charCodeAt(0) - 48);
1756
+ return out;
1757
+ }
1696
1758
  get solid() {
1697
1759
  return this._solid;
1698
1760
  }
@@ -1707,11 +1769,16 @@ var TileMap2D = class extends Node2D {
1707
1769
  /** Loader hook: bad cells fail at LOAD (unknown chars), not at render. */
1708
1770
  static validateJson(node) {
1709
1771
  const map = node;
1710
- parseCells(map._cells, map._legend);
1772
+ for (const [ch, value] of Object.entries(map._colors)) {
1773
+ if (typeof value === "string" && HEX.test(value)) continue;
1774
+ throw new IncantoError("PROP_TYPE_MISMATCH", `TileMap2D '${node.name}': "colors" ['${ch}'] is ${JSON.stringify(value)} — a colour here is \`#rgb\` or \`#rrggbb\`, the same as every other colour prop.`, { prop: "colors" });
1775
+ }
1776
+ if (map.texture !== "" && Object.keys(map._colors).length > 0) throw new IncantoError("PROP_TYPE_MISMATCH", `TileMap2D '${node.name}': "colors" and "texture" are exclusive — colours are for a level authored before the art arrives. Drop one of them.`, { prop: "colors" });
1777
+ parseCells(map._cells, map.allChars());
1711
1778
  }
1712
1779
  /** The parsed index grid (test/tooling hook). */
1713
1780
  grid() {
1714
- return parseCells(this._cells, this._legend);
1781
+ return parseCells(this._cells, this.allChars());
1715
1782
  }
1716
1783
  /**
1717
1784
  * Which CELL is under this world point — `null` outside the grid.
@@ -1809,7 +1876,7 @@ var TileMap2D = class extends Node2D {
1809
1876
  if (this._solid.length === 0) return;
1810
1877
  const ts = this.tileSize;
1811
1878
  const [sx, sy] = this.builtScale;
1812
- mergeSolidRects(this.grid(), new Set(this._solid)).forEach((r, i) => {
1879
+ mergeSolidRects(this.grid(), new Set(this.solidIndices())).forEach((r, i) => {
1813
1880
  const body = new StaticBody2D(`__tileSolid${i}`);
1814
1881
  body.position = [(r.x + r.w / 2) * ts * sx, (r.y + r.h / 2) * ts * sy];
1815
1882
  body.collider = {
@@ -1839,6 +1906,23 @@ var TileMap2D = class extends Node2D {
1839
1906
  super._syncObject2D(assets);
1840
1907
  const mesh = this._mesh();
1841
1908
  const mat = mesh.material;
1909
+ if (Object.keys(this._colors).length > 0) {
1910
+ mesh.visible = this.opacity > 0;
1911
+ mesh.renderOrder = this.effectiveRenderOrder;
1912
+ mat.opacity = this.opacity;
1913
+ if (mat.map !== null || !mat.vertexColors) {
1914
+ mat.map = null;
1915
+ mat.vertexColors = true;
1916
+ mat.needsUpdate = true;
1917
+ this.geometryDirty = true;
1918
+ }
1919
+ if (!this.geometryDirty) return;
1920
+ this.geometryDirty = false;
1921
+ this.builtColumns = 1;
1922
+ this.builtRows = 1;
1923
+ this.buildGeometry(mesh.geometry, 1, 1);
1924
+ return;
1925
+ }
1842
1926
  if (!this.texture || !assets) {
1843
1927
  mesh.visible = false;
1844
1928
  return;
@@ -1882,6 +1966,8 @@ var TileMap2D = class extends Node2D {
1882
1966
  const positions = [];
1883
1967
  const uvs = [];
1884
1968
  const indices = [];
1969
+ const palette = this.palette().map((hex) => new Color(hex));
1970
+ const colors = [];
1885
1971
  for (let y = 0; y < grid.length; y++) {
1886
1972
  const row = grid[y];
1887
1973
  for (let x = 0; x < row.length; x++) {
@@ -1893,18 +1979,24 @@ var TileMap2D = class extends Node2D {
1893
1979
  const y1 = -((y + 1) * ts);
1894
1980
  const base = positions.length / 3;
1895
1981
  positions.push(x0, y0, 0, x1, y0, 0, x1, y1, 0, x0, y1, 0);
1896
- const col = tile % columns;
1897
- const rowIdx = Math.floor(tile / columns);
1982
+ const col = tile >= COLOR_BASE ? 0 : tile % columns;
1983
+ const rowIdx = tile >= COLOR_BASE ? 0 : Math.floor(tile / columns);
1898
1984
  const u0 = col / columns;
1899
1985
  const u1 = (col + 1) / columns;
1900
1986
  const v0 = 1 - rowIdx / atlasRows;
1901
1987
  const v1 = 1 - (rowIdx + 1) / atlasRows;
1902
1988
  uvs.push(u0, v0, u1, v0, u1, v1, u0, v1);
1989
+ const paint = tile >= COLOR_BASE ? palette[tile - COLOR_BASE] : void 0;
1990
+ const r = paint?.r ?? 1;
1991
+ const g = paint?.g ?? 1;
1992
+ const b = paint?.b ?? 1;
1993
+ for (let v = 0; v < 4; v++) colors.push(r, g, b);
1903
1994
  indices.push(base, base + 2, base + 1, base, base + 3, base + 2);
1904
1995
  }
1905
1996
  }
1906
1997
  geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
1907
1998
  geometry.setAttribute("uv", new BufferAttribute(new Float32Array(uvs), 2));
1999
+ geometry.setAttribute("color", new BufferAttribute(new Float32Array(colors), 3));
1908
2000
  geometry.setIndex(indices);
1909
2001
  geometry.computeBoundingSphere();
1910
2002
  }
@@ -1,4 +1,4 @@
1
- import { b as Engine } from "./behavior-DZExDn9o.js";
1
+ import { b as Engine } from "./behavior-Bf9P9oB2.js";
2
2
  import { s as JsonObject } from "./rng-Bb-IutXB.js";
3
3
 
4
4
  //#region src/core/audit.d.ts
@@ -59,7 +59,7 @@ declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
59
59
  * Incanto fails loudly with machine-readable codes and messages that list valid
60
60
  * alternatives — AI agents self-correct on hard failures, not warnings.
61
61
  */
62
- type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NETWORK_UNAVAILABLE" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE" | "WEBGL_UNAVAILABLE";
62
+ type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NETWORK_UNAVAILABLE" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE" | "WRONG_DIMENSION" | "WEBGL_UNAVAILABLE";
63
63
  /**
64
64
  * Structured context mirrored from the prose message — tools and agents parse
65
65
  * THESE instead of regexing the message (which is for humans and may reword).
@@ -1,5 +1,5 @@
1
- import { F as handlerShadowWarnings, I as sceneDimension, Y as nodeRefWarnings, z as HudWidgetBase } from "./save-slots-BXVg148r.js";
2
- import { N as translationKey } from "./register-6DYnKZcy.js";
1
+ import { L as handlerShadowWarnings, R as sceneDimension, V as HudWidgetBase, Z as nodeRefWarnings } from "./save-slots-CEuJPUle.js";
2
+ import { N as translationKey } from "./register-5IYfooAN.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { u as getNodeType } from "./touch-BnCyPA0G.js";
5
5
  //#region src/core/audit.ts
@@ -174,7 +174,7 @@ function auditScene(scene, opts = {}) {
174
174
  /** Nodes whose script produces NOTHING but signals — see `SIGNAL_ONLY`. */
175
175
  const silentSignals = [];
176
176
  const groupRefs = [];
177
- const walk = (node, path, insideHud, staticRoot, transformed) => {
177
+ const walk = (node, path, insideHud, staticRoot, transformed, onShelf) => {
178
178
  const type = node.type ?? "";
179
179
  const props = node.props ?? {};
180
180
  const script = node.script?.name;
@@ -189,6 +189,11 @@ function auditScene(scene, opts = {}) {
189
189
  if (props.current === true) currentCameras += 1;
190
190
  if (script === "FollowCamera") followedCameras.push(path);
191
191
  }
192
+ if (type === "TileMap2D") {
193
+ const cells = Array.isArray(props.cells) ? props.cells : [];
194
+ const colours = props.colors ?? {};
195
+ if (cells.length > 0 && (props.texture ?? "") === "" && Object.keys(colours).length === 0) warnings.push(`${path}: TileMap2D has ${cells.length} row(s) and neither "texture" nor "colors", so nothing of it is drawn. Give it a "colors" entry per legend char to see the level before the art arrives. (If it is meant to be an invisible collision map over scenery you drew yourself, this warning is the price of saying so.)`);
196
+ }
192
197
  if (type === "CharacterController3D") cameraDrivers.push(path);
193
198
  if (type === "Terrain3D") terrains.push(path);
194
199
  if (DRAPERS.has(type) && (props.terrain ?? "") === "") autoDrapers.push(path);
@@ -219,7 +224,7 @@ function auditScene(scene, opts = {}) {
219
224
  const collider = props.collider;
220
225
  if (!collider || collider.shape === void 0) warnings.push(`${path}: ${type} has no collider — physics will skip it (add "collider": { "shape": ... }).`);
221
226
  }
222
- if (isHudWidget(type) && !insideHud) warnings.push(`${path}: ${type} works only as a descendant of a HudLayer — it will not render.`);
227
+ if (isHudWidget(type) && !insideHud && !onShelf) warnings.push(`${path}: ${type} works only as a descendant of a HudLayer — it will not render.`);
223
228
  if (staticRoot && ANIMATED_TYPES.has(type)) warnings.push(`${path}: ${type} is frozen inside static subtree '${staticRoot}' — it will not animate/update on screen.`);
224
229
  const nextStatic = staticRoot ?? (props.static === true ? node.name ?? path : null);
225
230
  const nextHud = insideHud || type === "HudLayer";
@@ -228,12 +233,14 @@ function auditScene(scene, opts = {}) {
228
233
  path,
229
234
  what: own
230
235
  } : transformed;
231
- for (const child of node.children ?? []) walk(child, `${path}/${child.name ?? "?"}`, nextHud, nextStatic, nextTransformed);
236
+ const nextShelf = onShelf || script === "PrefabShelf";
237
+ for (const child of node.children ?? []) walk(child, `${path}/${child.name ?? "?"}`, nextHud, nextStatic, nextTransformed, nextShelf);
232
238
  };
233
- walk(root, root.name ?? "root", false, root.props?.static === true ? root.name ?? "root" : null, transformOf(root.props ?? {}) ? {
239
+ const rootTransform = transformOf(root.props ?? {});
240
+ walk(root, root.name ?? "root", false, root.props?.static === true ? root.name ?? "root" : null, rootTransform ? {
234
241
  path: root.name ?? "root",
235
- what: transformOf(root.props ?? {})
236
- } : null);
242
+ what: rootTransform
243
+ } : null, root.script?.name === "PrefabShelf");
237
244
  if (needUid.length > 0) warnings.push(`these carry state a save keeps and have no uid to key it under, so the save comes back EMPTY and the load reports no problem: ${needUid.join(", ")}. Give each one a "uid" from newUid().`);
238
245
  if (terrains.length > 1 && autoDrapers.length > 0) warnings.push(`${autoDrapers.join(", ")}: this scene has ${terrains.length} Terrain3Ds (${terrains.join(", ")}) and these do not say which one they grow on — with no \`terrain\` path they take the FIRST one in the tree, so the rest drape to the wrong height with nothing reported. Name it: "terrain": "%Name".`);
239
246
  if (cameraDrivers.length > 1) warnings.push(`${cameraDrivers.join(", ")}: ${cameraDrivers.length} CharacterController3Ds drive the SAME camera (each one finds the first \`current\` camera in the tree), so the last of them wins every frame and the others have no view. For local co-op give each player a panel with \`createSplitScreen\` (one engine each); otherwise only one node may carry a controller.`);
@@ -113,7 +113,10 @@ var Behavior = class {
113
113
  */
114
114
  get engine() {
115
115
  const engine = this.node?.tree?.engine;
116
- if (!engine) throw new IncantoError("TREE_VIOLATION", `Behavior on '${this.node?.name ?? "?"}' is not attached to a running engine. If this came from \`onReady\` during a load, the scene does not know its engine yet: pass it — \`loadScene(json, { engine })\` — which is what createGame2D/3D and runScript do. Otherwise the node is not in a scene set on an Engine.`);
116
+ if (!engine) {
117
+ if (this.node?.freed) throw new IncantoError("TREE_VIOLATION", `Behavior on '${this.node.name}' has been FREED — a scene swap frees the tree it replaces, so \`this.engine\` (which reads \`this.node.tree\`) is gone with it. Hold it BEFORE the swap: \`const engine = this.engine; goToScene(engine, next);\` and use that afterwards.`);
118
+ throw new IncantoError("TREE_VIOLATION", `Behavior on '${this.node?.name ?? "?"}' is not attached to a running engine. If this came from \`onReady\` during a load, the scene does not know its engine yet: pass it — \`loadScene(json, { engine })\` — which is what createGame2D/3D and runScript do. Otherwise the node is not in a scene set on an Engine.`);
119
+ }
117
120
  return engine;
118
121
  }
119
122
  /** Shortcut for `this.engine.input`. */
@@ -669,7 +672,11 @@ var Node = class {
669
672
  */
670
673
  wiringPath() {
671
674
  const parts = [];
672
- for (let n = this; n; n = n._parent) parts.unshift(n._cloneOf ?? n._name);
675
+ for (let n = this; n; n = n._parent) {
676
+ const authored = n._authoredPath;
677
+ if (authored !== void 0) return parts.length === 0 ? authored : `${authored}/${parts.join("/")}`;
678
+ parts.unshift(n._cloneOf ?? n._name);
679
+ }
673
680
  return `/${parts.join("/")}`;
674
681
  }
675
682
  getNode(path) {
@@ -874,9 +881,23 @@ var Node = class {
874
881
  if (this._tree) this._tree._queueFree(this);
875
882
  else this.free();
876
883
  }
884
+ /**
885
+ * Has this node been torn down?
886
+ *
887
+ * A scene swap frees the tree it replaces, so the answer is `true` for every
888
+ * node of the scene you just left — which is exactly the state a behavior is
889
+ * in when it runs one more line after `goToScene`. Kept as a flag rather than
890
+ * inferred from `parent`/`tree`, because a DETACHED node (a prefab on a
891
+ * shelf, a clone waiting to be added) has neither and is perfectly alive.
892
+ */
893
+ get freed() {
894
+ return this._freed;
895
+ }
896
+ _freed = false;
877
897
  /** Immediately detach and tear down this node and its children. */
878
898
  free() {
879
899
  const tree = this._tree;
900
+ this._freed = true;
880
901
  for (const child of [...this._children]) child.free();
881
902
  this._parent?.removeChild(this);
882
903
  if (this._tree) this._propagateExitTree();
@@ -2064,6 +2085,36 @@ function inferSceneDimension(root) {
2064
2085
  function sceneDimension(json) {
2065
2086
  return json.dimension ?? inferSceneDimension(json.root);
2066
2087
  }
2088
+ /**
2089
+ * Every node whose type name belongs to the OTHER world, with its path.
2090
+ *
2091
+ * The two halves of the engine are strangers by design: `Renderer2D` walks
2092
+ * `Node2D`s and `Physics2D` tracks 2D bodies; the 3D pair do the same for
2093
+ * theirs, and neither knows the other's nodes exist. So a `MeshInstance3D`
2094
+ * dropped into a 2D scene loads, validates, appears in the tree, opens a full
2095
+ * inspector — and is never drawn and never simulated. The editor's add-node
2096
+ * list offers all of them, because the LOADER is what says no, and the loader
2097
+ * did not.
2098
+ *
2099
+ * The suffix is the authority here, as it already is for
2100
+ * {@link inferSceneDimension} and for the registry's missing-registrar hint.
2101
+ */
2102
+ function foreignDimensionNodes(root, dimension) {
2103
+ const wrong = dimension === "2d" ? "3D" : "2D";
2104
+ const out = [];
2105
+ const visit = (n, parent) => {
2106
+ if (typeof n !== "object" || n === null) return;
2107
+ const node = n;
2108
+ const path = `${parent}/${typeof node.name === "string" ? node.name : "?"}`;
2109
+ if (typeof node.type === "string" && node.type.endsWith(wrong)) out.push({
2110
+ path,
2111
+ type: node.type
2112
+ });
2113
+ if (Array.isArray(node.children)) for (const child of node.children) visit(child, path);
2114
+ };
2115
+ visit(root, "");
2116
+ return out;
2117
+ }
2067
2118
  //#endregion
2068
2119
  //#region src/core/scene/handler-collision.ts
2069
2120
  /**
@@ -4415,6 +4466,18 @@ function loadScene(json, opts) {
4415
4466
  validateHeader(sceneJson);
4416
4467
  validateStrings(sceneJson.strings);
4417
4468
  const dimension = sceneDimension(sceneJson);
4469
+ if (dimension) {
4470
+ const foreign = foreignDimensionNodes(sceneJson.root, dimension);
4471
+ if (foreign.length > 0) {
4472
+ const first = foreign[0];
4473
+ const others = foreign.length > 1 ? ` (and ${foreign.length - 1} more)` : "";
4474
+ const declared = sceneJson.dimension ? `declared "dimension": "${dimension}"` : `no "dimension" key, and its other nodes make it ${dimension}`;
4475
+ throw new IncantoError("WRONG_DIMENSION", `${first.type} '${first.path}'${others} is a ${dimension === "2d" ? "3D" : "2D"} node in a scene with ${declared}. The ${dimension} renderer never draws it and the ${dimension} physics never simulates it. Use the matching node type, or move it to its own scene.`, {
4476
+ path: first.path,
4477
+ nodeType: first.type
4478
+ });
4479
+ }
4480
+ }
4418
4481
  validateEnvironment(sceneJson.environment, dimension);
4419
4482
  inheritedStrings = {};
4420
4483
  inheritedAssets = {};
@@ -4837,7 +4900,10 @@ function buildNode(nj, opts, stack, parentPath, constants, bands = []) {
4837
4900
  rejectUnknownKeys(nj, NODE_KEYS, "node", nj.name ?? "");
4838
4901
  rejectWrongKinds(nj, nj.name ?? "");
4839
4902
  if (nj.instance !== void 0 && nj.type !== void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' declares both "type" and "instance" — they are mutually exclusive.`);
4840
- if (nj.instance !== void 0) return buildInstance(nj, opts, stack, path, constants, bands);
4903
+ if (nj.instance !== void 0) {
4904
+ if (nj.props !== void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' places an instance and declares "props" — a placement's props go in "overrides", which deep-merge onto the sub-scene root. "props" here is read by nothing.`, { prop: "props" });
4905
+ return buildInstance(nj, opts, stack, path, constants, bands);
4906
+ }
4841
4907
  if (nj.type === void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' must declare either "type" or "instance".`);
4842
4908
  const node = createNode(nj.type, resolveConstants(nj.props, constants), { orderGroup: bands });
4843
4909
  applyCommon(node, nj, opts, constants);
@@ -5145,6 +5211,7 @@ function duplicateNode(node, opts) {
5145
5211
  const clone = buildNodeJson(stripUids(serializeNode(node)), opts ?? rebuildOptions(node));
5146
5212
  markCloned(clone);
5147
5213
  markTemplateName(clone);
5214
+ markTemplateOrigin(clone, node);
5148
5215
  return clone;
5149
5216
  }
5150
5217
  /**
@@ -5196,6 +5263,37 @@ function markCloned(node) {
5196
5263
  function markTemplateName(node) {
5197
5264
  node._cloneOf = node.name;
5198
5265
  }
5266
+ /**
5267
+ * @internal Where the template this clone came from was AUTHORED.
5268
+ *
5269
+ * `_cloneOf` (the name) is enough only while clones land where the template
5270
+ * was authored — the `Spawner` case (`/Waves/Grunt` template, `/Waves/Grunt2`
5271
+ * clone) and no other. A game that clones from a SHELF authors the template at
5272
+ * `/Game/Prefabs/Tower` and adds the clone under `/Game/Towers`, so its audio
5273
+ * was logged at `/Game/Towers/Tower/Thud`: a path no scene file contains, and
5274
+ * one no report that reads the file can ever match. Recording the address
5275
+ * itself is what lets `wiringPath` answer with the authored node wherever the
5276
+ * clone ends up — including a clone OF a clone, which carries it forward.
5277
+ *
5278
+ * Taken while the source is still in a tree; a template detached before the
5279
+ * first clone (`PrefabShelf`, `SpawnSource`) records its own on the way out.
5280
+ */
5281
+ function markTemplateOrigin(clone, source) {
5282
+ const origin = source._authoredPath ?? (source.tree ? source.getPath() : null);
5283
+ if (origin) clone._authoredPath = origin;
5284
+ }
5285
+ /**
5286
+ * @internal Remember where a node was authored, before it leaves the tree.
5287
+ *
5288
+ * A shelf or a spawner lifts its templates out at enter; from then on the
5289
+ * template cannot say where it came from, and neither can anything cloned from
5290
+ * it. Called on the way out, while the answer is still there.
5291
+ */
5292
+ function rememberAuthoredPath(node) {
5293
+ const at = node;
5294
+ if (at._authoredPath !== void 0) return;
5295
+ if (node.tree || node.parent?.tree) at._authoredPath = node.getPath();
5296
+ }
5199
5297
  /** @internal Was this node made at runtime rather than loaded from a file? */
5200
5298
  function isCloned(node) {
5201
5299
  return node._cloned === true;
@@ -5648,4 +5746,4 @@ function savesWithoutUid(root) {
5648
5746
  return out;
5649
5747
  }
5650
5748
  //#endregion
5651
- export { StubBehavior as $, resetMemorySaves as A, TAPPABLE as B, isConstRef as C, newUid as D, resolveViewport as E, handlerShadowWarnings as F, requireText as G, UiBar as H, sceneDimension as I, describeRefProblem as J, Node as K, FITS$1 as L, effectiveOrder as M, resolveOrderGroups as N, SceneTree as O, InputMap as P, Behavior as Q, HudLayer as R, CONST_REF_KEY as S, computeViewport as T, UiText as U, UiBanner as V, requireNumber as W, resolveRefInJson as X, nodeRefWarnings as Y, parseNodePath as Z, SCENE_FORMAT as _, restoreBehaviors as a, registeredBehaviors as at, sunDirectionFromElevationAzimuth as b, markCloned as c, stripUids as d, behaviorSchema as et, buildSceneSubtree as f, serializeNode as g, Scene as h, captureBehaviors as i, registerBehavior as it, ORDER_GROUP_BASE as j, createSaveStore as k, markTemplateName as l, loader_exports as m, SaveSlots as n, clearBehaviors as nt, savesWithoutUid as o, Signal as ot, loadScene as p, diagnose as q, behaviorsWithoutSave as r, getBehavior as rt, duplicateNode as s, ENGINE_STATE_KEY as t, behaviorSignals as tt, rebuildOptions as u, horizonColorFromSky as v, resolveConstants as w, sunDirectionFromSky as x, parseEnvironment3D as y, HudWidgetBase as z };
5749
+ export { parseNodePath as $, SceneTree as A, HudLayer as B, sunDirectionFromSky as C, computeViewport as D, resolveConstants as E, resolveOrderGroups as F, UiText as G, TAPPABLE as H, InputMap as I, Node as J, requireNumber as K, handlerShadowWarnings as L, resetMemorySaves as M, ORDER_GROUP_BASE as N, resolveViewport as O, effectiveOrder as P, resolveRefInJson as Q, sceneDimension as R, sunDirectionFromElevationAzimuth as S, isConstRef as T, UiBanner as U, HudWidgetBase as V, UiBar as W, describeRefProblem as X, diagnose as Y, nodeRefWarnings as Z, Scene as _, restoreBehaviors as a, getBehavior as at, horizonColorFromSky as b, isCloned as c, Signal as ct, rebuildOptions as d, Behavior as et, rememberAuthoredPath as f, loader_exports as g, loadScene as h, captureBehaviors as i, clearBehaviors as it, createSaveStore as j, newUid as k, markCloned as l, buildSceneSubtree as m, SaveSlots as n, behaviorSchema as nt, savesWithoutUid as o, registerBehavior as ot, stripUids as p, requireText as q, behaviorsWithoutSave as r, behaviorSignals as rt, duplicateNode as s, registeredBehaviors as st, ENGINE_STATE_KEY as t, StubBehavior as tt, markTemplateName as u, serializeNode as v, CONST_REF_KEY as w, parseEnvironment3D as x, SCENE_FORMAT as y, FITS$1 as z };
@@ -1,6 +1,6 @@
1
1
  import { n as jsonEquals, t as jsonClone } from "./json-CfTjpvW8.js";
2
- import { K as Node, c as markCloned, d as stripUids, f as buildSceneSubtree, l as markTemplateName, ot as Signal, p as loadScene, q as diagnose, u as rebuildOptions } from "./save-slots-BXVg148r.js";
3
- import { t as registerCoreNodes, v as Engine } from "./register-6DYnKZcy.js";
2
+ import { J as Node, Y as diagnose, ct as Signal, d as rebuildOptions, h as loadScene, l as markCloned, m as buildSceneSubtree, p as stripUids, u as markTemplateName } from "./save-slots-CEuJPUle.js";
3
+ import { t as registerCoreNodes, v as Engine } from "./register-5IYfooAN.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { p as registerNode } from "./touch-BnCyPA0G.js";
6
6
  //#region src/net/loopback.ts
@@ -1150,7 +1150,7 @@ function applySyncPatch(root, patch, setProp, onMissingNode) {
1150
1150
  }
1151
1151
  }
1152
1152
  async function createDefaultTransport(config, diagnostics) {
1153
- const { createAgent8Server } = await import("./agent8-CmNF01gA.js").then((n) => n.t);
1153
+ const { createAgent8Server } = await import("./agent8-BqZ0_O1u.js").then((n) => n.t);
1154
1154
  return createAgent8Server(config, diagnostics);
1155
1155
  }
1156
1156
  //#endregion
@@ -1,7 +1,7 @@
1
- import { Lt as Signal, b as Engine, jt as Node } from "./behavior-DZExDn9o.js";
1
+ import { Lt as Signal, b as Engine, jt as Node } from "./behavior-Bf9P9oB2.js";
2
2
  import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
3
3
  import { a as SceneJson, c as JsonValue, s as JsonObject } from "./rng-Bb-IutXB.js";
4
- import { t as LoadSceneOptions } from "./loader-Cff09LMm.js";
4
+ import { t as LoadSceneOptions } from "./loader-CBfQzB6S.js";
5
5
 
6
6
  //#region src/net/types.d.ts
7
7
  type Unsubscribe = () => void;
@@ -199,6 +199,6 @@ async function preloadSceneAssets(assets, title) {
199
199
  //#endregion
200
200
  //#region src/index.ts
201
201
  /** Engine version. Kept in sync with package.json by the release pipeline. */
202
- const VERSION = "0.70.0";
202
+ const VERSION = "0.72.0";
203
203
  //#endregion
204
204
  export { findPath as a, preloadUrls as i, assetUrls as n, gridFromRows as o, preloadSceneAssets as r, VERSION as t };