incanto 0.69.0 → 0.71.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
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-B6ugCV1Q.js";
1
+ import { a as SceneJson, r as NodeJson, s as JsonObject, t as Rng } from "./rng-Bb-IutXB.js";
2
2
 
3
3
  //#region src/env/arena.d.ts
4
4
  /** Valid `generateArena` themes — drives the catalog options AND validation. */
package/dist/env.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { t as Rng } from "./rng-DP-SR7eg.js";
3
- import { t as createNoise2D } from "./noise-CGUMx44x.js";
4
- import { t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
2
+ import { t as Rng } from "./rng-CDOMybym.js";
3
+ import { t as createNoise2D } from "./noise-D3nPpmFg.js";
4
+ import { t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
5
5
  //#region src/env/round.ts
6
6
  /** 2-decimal rounding — keeps generated JSON readable without hurting determinism. */
7
7
  function round2(value) {
@@ -147,6 +147,7 @@ function rock(index, x, z, rng, sizeRange, color) {
147
147
  round2(radius * squash * .6),
148
148
  z
149
149
  ],
150
+ snapToGround: false,
150
151
  rotation: [
151
152
  0,
152
153
  rng.int(0, 359),
@@ -1400,6 +1401,7 @@ function generateClouds(opts) {
1400
1401
  return {
1401
1402
  name: "Clouds",
1402
1403
  type: "Node3D",
1404
+ props: { snapToGround: false },
1403
1405
  children
1404
1406
  };
1405
1407
  }
@@ -1,13 +1,13 @@
1
- import { k as diagnose } from "./loader-DZE7B07H.js";
2
- import { L as translateOn, t as registerCoreNodes } from "./register-mmfugpaQ.js";
1
+ import { Y as diagnose } from "./save-slots-Bvuh2p_r.js";
2
+ import { M as translateOn, t as registerCoreNodes } from "./register-DuVdzxbq.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
- import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
- import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
8
- import { B as rejectScale, G as StaticBody3D, H as CharacterBody3D, I as worldPosition, K as Node3D, L as Water3D, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, w as tolerateUnknownAction, z as WaterCutout3D } from "./gameplay-D1KixWK8.js";
4
+ import { t as Rng } from "./rng-CDOMybym.js";
5
+ import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
6
+ import { t as createNoise2D } from "./noise-D3nPpmFg.js";
7
+ 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";
8
+ import { B as rejectScale, D as effectRng, G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, Q as worldPosition, T as tolerateUnknownAction, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, z as WaterCutout3D } from "./gameplay-CNULJvwh.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
10
- import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
10
+ import { n as splatWeights, t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
11
11
  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";
12
12
  import { clone } from "three/addons/utils/SkeletonUtils.js";
13
13
  //#region src/3d/textures.ts
@@ -141,7 +141,7 @@ var TextureCache3D = class {
141
141
  return this.entries.size;
142
142
  }
143
143
  dispose() {
144
- for (const entry of this.entries.values()) entry.texture.dispose();
144
+ for (const entry of this.entries.values()) entry.texture?.dispose();
145
145
  this.entries.clear();
146
146
  }
147
147
  };
@@ -716,7 +716,7 @@ var InstancedMesh3D = class extends Node3D {
716
716
  for (let i = 0; i < this._transforms.length; i++) {
717
717
  const row = this._transforms[i];
718
718
  posScratch.set(row[0] ?? 0, row[1] ?? 0, row[2] ?? 0);
719
- quatScratch$2.setFromAxisAngle(UP$1, (row[3] ?? 0) * Math.PI / 180);
719
+ quatScratch$2.setFromAxisAngle(UP$2, (row[3] ?? 0) * Math.PI / 180);
720
720
  const s = row[4] ?? 1;
721
721
  scaleScratch$3.set(s, s, s);
722
722
  matScratch.compose(posScratch, quatScratch$2, scaleScratch$3);
@@ -734,7 +734,7 @@ var InstancedMesh3D = class extends Node3D {
734
734
  super.free();
735
735
  }
736
736
  };
737
- const UP$1 = new Vector3(0, 1, 0);
737
+ const UP$2 = new Vector3(0, 1, 0);
738
738
  const posScratch = new Vector3();
739
739
  const quatScratch$2 = new Quaternion();
740
740
  const scaleScratch$3 = new Vector3();
@@ -835,6 +835,10 @@ var Joint3D = class extends Node3D {
835
835
  const a = this.parent;
836
836
  const b = this.getNodeOrNull(this.target);
837
837
  if (!(b instanceof PhysicsBody3D)) throw new IncantoError("NODE_NOT_FOUND", `Joint3D '${this.name}': target '${this.target}' is not a physics body.`);
838
+ if (a === b) throw new IncantoError("BAD_FORMAT", `Joint3D '${this.name}': target '${this.target}' is the SAME body this joint hangs off, so it constrains nothing. A joint is a child of ONE body pointing at the OTHER — name the other body (e.g. "%Crate").`, {
839
+ prop: "target",
840
+ path: this.getPath()
841
+ });
838
842
  return {
839
843
  a,
840
844
  b
@@ -1567,9 +1571,18 @@ var Terrain3D = class extends Node3D {
1567
1571
  default: "island",
1568
1572
  options: TERRAIN_THEMES
1569
1573
  },
1570
- roughness: { default: .5 },
1574
+ roughness: {
1575
+ default: .5,
1576
+ range: {
1577
+ min: 0,
1578
+ max: 1
1579
+ }
1580
+ },
1571
1581
  detail: { default: 4 },
1572
- flatThreshold: { default: .95 },
1582
+ flatThreshold: {
1583
+ default: .95,
1584
+ range: { min: 0 }
1585
+ },
1573
1586
  islandEdge: { default: null },
1574
1587
  layers: { default: [] },
1575
1588
  textureBase: { default: DEFAULT_TERRAIN_TEXTURE_BASE },
@@ -1591,7 +1604,32 @@ var Terrain3D = class extends Node3D {
1591
1604
  roughness = .5;
1592
1605
  /** Detail octave count. */
1593
1606
  detail = 4;
1594
- /** Snap-flatten a plateau within ±0.6 m of maxHeight·flatThreshold. */
1607
+ /**
1608
+ * Snap-flatten a plateau within ±0.6 m of maxHeight·flatThreshold.
1609
+ *
1610
+ * A fraction of `maxHeight`, which is a SCALE and not the peak: heights
1611
+ * realize at roughly 3.5–8× it, so the default 0.95 puts the plateau near the
1612
+ * BOTTOM of the range rather than at the summit the name suggests. Measured
1613
+ * on an alpine 200 m terrain, seed 3:
1614
+ *
1615
+ * ```
1616
+ * maxHeight 28 realized 25..150 band at 26.6 = 18% of peak, 28 cells on it
1617
+ * maxHeight 8 realized 8..43 band at 7.6 = 18% of peak, 99 cells on it
1618
+ * ```
1619
+ *
1620
+ * It is doing something — that is a real terrace, and the ±0.6 m boundary
1621
+ * between snapped and unsnapped cells is what `examples/water-ocean-3d` calls
1622
+ * "a sawtooth band across the underwater flat". Read `heightAt` before
1623
+ * choosing a value, the way `maxHeight`'s own note says to.
1624
+ *
1625
+ * 0..1 is the useful range, and anything ABOVE 1 turns the snap off, because
1626
+ * the band then sits over every height the terrain realizes. `examples/water-ocean-3d` uses `20` for exactly that,
1627
+ * with a comment explaining why, which is how a `max: 1` on this prop was
1628
+ * caught the moment it was declared: at `maxHeight 0.1` the ±0.6 m band
1629
+ * swallows the whole field and rips a sawtooth across the underwater flat.
1630
+ * So the bound is `min: 0` and "off" is a documented idiom rather than an
1631
+ * accident.
1632
+ */
1595
1633
  flatThreshold = .95;
1596
1634
  /** Wrap edges down into an island rim. null = theme decides (island: on). */
1597
1635
  islandEdge = null;
@@ -1963,7 +2001,13 @@ const SHADOW_CONTACT_PUSH = .5;
1963
2001
  var Sprite3D = class extends Node3D {
1964
2002
  static typeName = "Sprite3D";
1965
2003
  static props = {
1966
- texture: { default: "" },
2004
+ texture: {
2005
+ default: "",
2006
+ asset: {
2007
+ kind: "texture",
2008
+ urlOnly: true
2009
+ }
2010
+ },
1967
2011
  size: { default: [1, 1] },
1968
2012
  billboard: {
1969
2013
  default: "y",
@@ -1975,10 +2019,22 @@ var Sprite3D = class extends Node3D {
1975
2019
  },
1976
2020
  anchor: { default: [.5, 0] },
1977
2021
  tint: { default: "#ffffff" },
1978
- opacity: { default: 1 },
2022
+ opacity: {
2023
+ default: 1,
2024
+ range: {
2025
+ min: 0,
2026
+ max: 1
2027
+ }
2028
+ },
1979
2029
  flipX: { default: false },
1980
2030
  pixelArt: { default: true },
1981
- alphaTest: { default: .5 },
2031
+ alphaTest: {
2032
+ default: .5,
2033
+ range: {
2034
+ min: 0,
2035
+ max: 1
2036
+ }
2037
+ },
1982
2038
  castShadow: { default: false },
1983
2039
  shadowMode: {
1984
2040
  default: "silhouette",
@@ -2052,7 +2108,7 @@ var Sprite3D = class extends Node3D {
2052
2108
  mat.opacity = this.opacity;
2053
2109
  mat.transparent = this.opacity < 1;
2054
2110
  mat.alphaTest = this.alphaTest;
2055
- quad.renderOrder = this.renderOrder;
2111
+ quad.renderOrder = this.effectiveRenderOrder;
2056
2112
  const wantShadow = this.castShadow && tex !== null;
2057
2113
  quad.castShadow = false;
2058
2114
  if (wantShadow && this.shadowMode === "silhouette") {
@@ -2159,7 +2215,6 @@ var Sprite3D = class extends Node3D {
2159
2215
  mesh.quaternion.copy(_parent.invert()).multiply(_target);
2160
2216
  }
2161
2217
  free() {
2162
- this.loadedTexture?.dispose();
2163
2218
  this.loadedTexture = null;
2164
2219
  if (this.shadowMesh) {
2165
2220
  this.shadowMesh.material.dispose();
@@ -2181,7 +2236,13 @@ var AnimatedSprite3D = class extends Sprite3D {
2181
2236
  static typeName = "AnimatedSprite3D";
2182
2237
  static signals = ["animationFinished"];
2183
2238
  static props = {
2184
- sheet: { default: "" },
2239
+ sheet: {
2240
+ default: "",
2241
+ asset: {
2242
+ kind: "spritesheet",
2243
+ urlOnly: true
2244
+ }
2245
+ },
2185
2246
  frameWidth: { default: 0 },
2186
2247
  frameHeight: { default: 0 },
2187
2248
  animations: { default: {} },
@@ -2223,7 +2284,7 @@ var AnimatedSprite3D = class extends Sprite3D {
2223
2284
  this.playing = false;
2224
2285
  }
2225
2286
  onReady() {
2226
- validateAnimationAliases(this.animations, this.name);
2287
+ validateAnimations(this.animations, this.name);
2227
2288
  if (this.autoplay) this.play(this.autoplay);
2228
2289
  }
2229
2290
  update(dt) {
@@ -2560,15 +2621,15 @@ var BoneLookAt3D = class extends Node3D {
2560
2621
  if (targetNode && Array.isArray(targetNode.position)) {
2561
2622
  bone.updateWorldMatrix(true, false);
2562
2623
  bone.getWorldPosition(bonePosScratch);
2563
- targetScratch.set(targetNode.position[0] ?? 0, targetNode.position[1] ?? 0, targetNode.position[2] ?? 0);
2564
- dirScratch$1.copy(targetScratch).sub(bonePosScratch);
2624
+ targetScratch$1.set(targetNode.position[0] ?? 0, targetNode.position[1] ?? 0, targetNode.position[2] ?? 0);
2625
+ dirScratch$1.copy(targetScratch$1).sub(bonePosScratch);
2565
2626
  if (dirScratch$1.lengthSq() > 1e-6) {
2566
2627
  dirScratch$1.normalize();
2567
2628
  axisScratch.copy(AXES[this.forwardAxis] ?? PLUS_Z);
2568
2629
  animForwardScratch.copy(axisScratch).applyQuaternion(bone.getWorldQuaternion(quatScratch$1));
2569
2630
  if (animForwardScratch.angleTo(dirScratch$1) < this.maxAngleDeg * Math.PI / 180) {
2570
2631
  engaged = true;
2571
- lookMatrixScratch.lookAt(bonePosScratch, targetScratch, UP);
2632
+ lookMatrixScratch.lookAt(bonePosScratch, targetScratch$1, UP$1);
2572
2633
  desiredScratch.setFromRotationMatrix(lookMatrixScratch);
2573
2634
  correctionScratch.setFromUnitVectors(axisScratch, MINUS_Z);
2574
2635
  desiredScratch.multiply(correctionScratch);
@@ -2591,11 +2652,11 @@ const AXES = {
2591
2652
  "+z": new Vector3(0, 0, 1),
2592
2653
  "-z": new Vector3(0, 0, -1)
2593
2654
  };
2594
- const UP = new Vector3(0, 1, 0);
2655
+ const UP$1 = new Vector3(0, 1, 0);
2595
2656
  const PLUS_Z = new Vector3(0, 0, 1);
2596
2657
  const MINUS_Z = new Vector3(0, 0, -1);
2597
2658
  const bonePosScratch = new Vector3();
2598
- const targetScratch = new Vector3();
2659
+ const targetScratch$1 = new Vector3();
2599
2660
  const dirScratch$1 = new Vector3();
2600
2661
  const axisScratch = new Vector3();
2601
2662
  const animForwardScratch = new Vector3();
@@ -2606,6 +2667,13 @@ const parentScratch = new Quaternion();
2606
2667
  const quatScratch$1 = new Quaternion();
2607
2668
  //#endregion
2608
2669
  //#region src/3d/nodes/camera-3d.ts
2670
+ const RAD2DEG$1 = 180 / Math.PI;
2671
+ /** World up. The whole fix is that this argument exists. */
2672
+ const UP = new Vector3(0, 1, 0);
2673
+ const eyeScratch = new Vector3();
2674
+ const targetScratch = new Vector3();
2675
+ const lookScratch = new Matrix4();
2676
+ const eulerScratch$1 = new Euler(0, 0, 0, "XYZ");
2609
2677
  /**
2610
2678
  * A perspective camera. Mark exactly one camera `current: true`; with none
2611
2679
  * marked, the renderer falls back to the first camera in tree order.
@@ -2614,15 +2682,79 @@ const quatScratch$1 = new Quaternion();
2614
2682
  var Camera3D = class extends Node3D {
2615
2683
  static typeName = "Camera3D";
2616
2684
  static props = {
2617
- fov: { default: 60 },
2618
- near: { default: .1 },
2619
- far: { default: 1e3 },
2620
- current: { default: false }
2685
+ fov: {
2686
+ default: 60,
2687
+ range: {
2688
+ min: 0,
2689
+ max: 180,
2690
+ exclusiveMin: true,
2691
+ exclusiveMax: true
2692
+ }
2693
+ },
2694
+ near: {
2695
+ default: .1,
2696
+ range: {
2697
+ min: 0,
2698
+ exclusiveMin: true
2699
+ }
2700
+ },
2701
+ far: {
2702
+ default: 1e3,
2703
+ range: {
2704
+ min: 0,
2705
+ exclusiveMin: true
2706
+ }
2707
+ },
2708
+ current: { default: false },
2709
+ lookAt: {
2710
+ default: "",
2711
+ nodePath: true
2712
+ }
2621
2713
  };
2714
+ /** A frustum whose planes are the wrong way round draws nothing at all. */
2715
+ static validateJson(node) {
2716
+ const self = node;
2717
+ if (self.near >= self.far) throw new IncantoError("PROP_TYPE_MISMATCH", `Camera3D '${node.name}' has an inverted depth range: near ${self.near} is not less than far ${self.far}. The near plane is the closest thing this camera can see and the far plane the furthest.`);
2718
+ }
2622
2719
  fov = 60;
2623
2720
  near = .1;
2624
2721
  far = 1e3;
2625
2722
  current = false;
2723
+ lookAt = "";
2724
+ /**
2725
+ * Aim, every frame, whenever `lookAt` names something.
2726
+ *
2727
+ * In `update` and not `_syncObject3D`: the second is the renderer's copy pass
2728
+ * and a static node is pruned from it, which silently freezes anything driven
2729
+ * there. `rotation` is written as an ordinary prop, so the editor, the
2730
+ * serializer and `describeFraming` all see what the camera is actually doing.
2731
+ */
2732
+ /** The `lookAt` path already reported as unlookable — cleared by a new one. */
2733
+ warnedLookAt = "";
2734
+ update(_dt) {
2735
+ if (this.lookAt === "") return;
2736
+ const target = this.getNodeOrNull(this.lookAt);
2737
+ if (!target) return;
2738
+ const to = worldPosition(target);
2739
+ if (to.length < 3) {
2740
+ if (this.warnedLookAt !== this.lookAt) {
2741
+ this.warnedLookAt = this.lookAt;
2742
+ diagnose(this.tree?.engine ?? null, "warn", `[incanto] ${this.getPath()}: lookAt '${this.lookAt}' resolves to a ${target.constructor.typeName ?? "Node"}, which has no world position — the camera keeps its authored rotation. Point it at a Node3D.`);
2743
+ }
2744
+ return;
2745
+ }
2746
+ const from = worldPosition(this);
2747
+ eyeScratch.set(from[0] ?? 0, from[1] ?? 0, from[2] ?? 0);
2748
+ targetScratch.set(to[0] ?? 0, to[1] ?? 0, to[2] ?? 0);
2749
+ if (eyeScratch.distanceToSquared(targetScratch) < 1e-12) return;
2750
+ lookScratch.lookAt(eyeScratch, targetScratch, UP);
2751
+ eulerScratch$1.setFromRotationMatrix(lookScratch, "XYZ");
2752
+ this.rotation = [
2753
+ eulerScratch$1.x * RAD2DEG$1,
2754
+ eulerScratch$1.y * RAD2DEG$1,
2755
+ eulerScratch$1.z * RAD2DEG$1
2756
+ ];
2757
+ }
2626
2758
  _createObject3D() {
2627
2759
  return new PerspectiveCamera(this.fov, 1, this.near, this.far);
2628
2760
  }
@@ -2639,6 +2771,17 @@ var Camera3D = class extends Node3D {
2639
2771
  };
2640
2772
  //#endregion
2641
2773
  //#region src/3d/nodes/character-controller-3d.ts
2774
+ /**
2775
+ * The states this controller enters, and therefore the only keys its
2776
+ * `animations` map may carry.
2777
+ */
2778
+ const MOVEMENT_STATES = [
2779
+ "idle",
2780
+ "walk",
2781
+ "run",
2782
+ "fastRun",
2783
+ "airborne"
2784
+ ];
2642
2785
  const ACC_DELTA_TIME = 8;
2643
2786
  const DRAG_DAMPING = .15;
2644
2787
  const SPRING_K = 1.2;
@@ -2725,7 +2868,13 @@ var CharacterController3D = class extends Node3D {
2725
2868
  nodePath: true
2726
2869
  },
2727
2870
  skinYawOffset: { default: 0 },
2728
- animations: { default: {} }
2871
+ animations: {
2872
+ default: {},
2873
+ asset: {
2874
+ kind: "animation",
2875
+ mapValues: true
2876
+ }
2877
+ }
2729
2878
  };
2730
2879
  view = "free";
2731
2880
  camDistance = 4;
@@ -2825,6 +2974,27 @@ var CharacterController3D = class extends Node3D {
2825
2974
  * "walk": "$walk", "airborne": "$jump" }
2826
2975
  */
2827
2976
  animations = {};
2977
+ /**
2978
+ * Scene-load validation: an animation keyed by a state this controller never
2979
+ * enters is a clip that never plays.
2980
+ *
2981
+ * The five state names live in one place in one skill, and `jumping`,
2982
+ * `sprint`, `walking` and `falling` are what they are called in most other
2983
+ * engines — `jump` is even the ACTION name in every template here. A wrong
2984
+ * key was accepted and never looked at again, so the character walked and
2985
+ * idled correctly and never animated its jump, which reads as broken
2986
+ * animation rather than a wrong key.
2987
+ */
2988
+ static validateJson(node) {
2989
+ const ctl = node;
2990
+ for (const state of Object.keys(ctl.animations)) {
2991
+ if (MOVEMENT_STATES.includes(state)) continue;
2992
+ throw new IncantoError("BAD_FORMAT", `CharacterController3D '${node.name}' maps an animation to '${state}', which is not a movement state.${didYouMean(state, MOVEMENT_STATES)} The states are [${MOVEMENT_STATES.join(", ")}].`, {
2993
+ prop: "animations",
2994
+ validOptions: [...MOVEMENT_STATES]
2995
+ });
2996
+ }
2997
+ }
2828
2998
  /** Camera yaw/pitch in radians (mouse look mutates these). */
2829
2999
  yaw = 0;
2830
3000
  pitch = 0;
@@ -3793,7 +3963,8 @@ var Flowers3D = class extends Node3D {
3793
3963
  static props = {
3794
3964
  density: {
3795
3965
  default: "sparse",
3796
- options: DENSITY_OPTIONS
3966
+ options: DENSITY_OPTIONS,
3967
+ kinds: ["string", "number"]
3797
3968
  },
3798
3969
  area: { default: [20, 20] },
3799
3970
  seed: { default: 1 },
@@ -5189,7 +5360,10 @@ var Foliage3D = class Foliage3D extends Node3D {
5189
5360
  options: TUFT_STYLES
5190
5361
  },
5191
5362
  area: { default: [20, 20] },
5192
- density: { default: 12 },
5363
+ density: {
5364
+ default: 12,
5365
+ range: { min: 0 }
5366
+ },
5193
5367
  maxInstances: { default: 5e4 },
5194
5368
  height: { default: .25 },
5195
5369
  colorA: { default: "#4d7232" },
@@ -5204,7 +5378,13 @@ var Foliage3D = class Foliage3D extends Node3D {
5204
5378
  fadeEnd: { default: 90 },
5205
5379
  sway: { default: .6 },
5206
5380
  interaction: { default: true },
5207
- coverage: { default: .85 },
5381
+ coverage: {
5382
+ default: .85,
5383
+ range: {
5384
+ min: 0,
5385
+ max: 1
5386
+ }
5387
+ },
5208
5388
  flowers: { default: 0 },
5209
5389
  seed: { default: 1 },
5210
5390
  drape: { default: null },
@@ -6053,7 +6233,10 @@ var DirectionalLight3D = class extends Node3D {
6053
6233
  static typeName = "DirectionalLight3D";
6054
6234
  static props = {
6055
6235
  color: { default: "#ffffff" },
6056
- intensity: { default: 1 },
6236
+ intensity: {
6237
+ default: 1,
6238
+ range: { min: 0 }
6239
+ },
6057
6240
  castShadow: { default: false },
6058
6241
  shadowArea: { default: 75 },
6059
6242
  shadowMapSize: { default: 2048 },
@@ -6104,7 +6287,10 @@ var OmniLight3D = class extends Node3D {
6104
6287
  static typeName = "OmniLight3D";
6105
6288
  static props = {
6106
6289
  color: { default: "#ffffff" },
6107
- intensity: { default: 1 },
6290
+ intensity: {
6291
+ default: 1,
6292
+ range: { min: 0 }
6293
+ },
6108
6294
  range: { default: 0 }
6109
6295
  };
6110
6296
  color = "#ffffff";
@@ -6634,7 +6820,10 @@ var ModelInstance3D = class extends Node3D {
6634
6820
  /** Seconds into the clip to start at — stagger a row of identical props. */
6635
6821
  animationOffset: { default: 0 },
6636
6822
  /** Upper-body layer clip ('' = off). One-shots auto-clear on finish. */
6637
- animationUpper: { default: "" },
6823
+ animationUpper: {
6824
+ default: "",
6825
+ asset: { kind: "animation" }
6826
+ },
6638
6827
  /** Loop the upper layer (false = one-shot: attack, wave, throw). */
6639
6828
  animationUpperLoop: { default: false },
6640
6829
  /** Bone whose subtree the upper layer drives (forgiving lookup). */
@@ -6799,6 +6988,15 @@ var ModelInstance3D = class extends Node3D {
6799
6988
  return [...embedded, ...assets];
6800
6989
  }
6801
6990
  /** Called by the 3D sync pass with the renderer's asset store. */
6991
+ /**
6992
+ * A model is ready when its GLB is. Freezing before that mounts nothing —
6993
+ * see `Node3D._staticReady`.
6994
+ */
6995
+ _staticReady(assets) {
6996
+ if (!this.model) return true;
6997
+ if (!assets) return false;
6998
+ return assets.getModel(this.model).status === "ready";
6999
+ }
6802
7000
  _syncModel(assets) {
6803
7001
  this.store = assets;
6804
7002
  const root = this._ensureObject3D();
@@ -7205,8 +7403,20 @@ var Particles3D = class Particles3D extends Node3D {
7205
7403
  colorStart: { default: "#ffffff" },
7206
7404
  colorEnd: { default: "#ffffff" },
7207
7405
  paletteColors: { default: [] },
7208
- alphaStart: { default: 1 },
7209
- alphaEnd: { default: 0 },
7406
+ alphaStart: {
7407
+ default: 1,
7408
+ range: {
7409
+ min: 0,
7410
+ max: 1
7411
+ }
7412
+ },
7413
+ alphaEnd: {
7414
+ default: 0,
7415
+ range: {
7416
+ min: 0,
7417
+ max: 1
7418
+ }
7419
+ },
7210
7420
  shimmer: { default: 0 },
7211
7421
  blend: {
7212
7422
  default: "add",
@@ -7276,7 +7486,7 @@ var Particles3D = class Particles3D extends Node3D {
7276
7486
  _ensureSim() {
7277
7487
  if (!this.sim) {
7278
7488
  applyParticlePreset(this, this.preset, getNodeSchema(Particles3D.typeName));
7279
- const rng = this.tree?.engine?.rng ?? new Rng(4660);
7489
+ const rng = effectRng(this);
7280
7490
  this.sim = new ParticleSim({
7281
7491
  rate: this.emitting ? this.rate : 0,
7282
7492
  lifetime: [this.lifetime[0] ?? 1, this.lifetime[1] ?? 1],
@@ -7368,6 +7578,7 @@ var Particles3D = class Particles3D extends Node3D {
7368
7578
  kind,
7369
7579
  name: this.preset,
7370
7580
  from: this.getPath(),
7581
+ wiredAs: this.wiringPath(),
7371
7582
  amount,
7372
7583
  at: engine.time
7373
7584
  });
@@ -7435,7 +7646,7 @@ var Particles3D = class Particles3D extends Node3D {
7435
7646
  this.points.frustumCulled = false;
7436
7647
  this._ensureObject3D().add(this.points);
7437
7648
  }
7438
- this.points.renderOrder = this.renderOrder;
7649
+ this.points.renderOrder = this.effectiveRenderOrder;
7439
7650
  const material = this.points.material;
7440
7651
  material.depthTest = this.depthTest;
7441
7652
  material.blending = this.blend === "add" ? AdditiveBlending : NormalBlending;
@@ -8867,7 +9078,13 @@ var River3D = class River3D extends Node3D {
8867
9078
  flowSpeed: { default: 1.6 },
8868
9079
  colors: { default: {} },
8869
9080
  absorption: { default: .5 },
8870
- opacity: { default: .85 },
9081
+ opacity: {
9082
+ default: .85,
9083
+ range: {
9084
+ min: 0,
9085
+ max: 1
9086
+ }
9087
+ },
8871
9088
  foam: { default: 1 },
8872
9089
  ripples: { default: 1 },
8873
9090
  sunDirection: { default: [
@@ -8876,7 +9093,10 @@ var River3D = class River3D extends Node3D {
8876
9093
  .3
8877
9094
  ] },
8878
9095
  sunColor: { default: "#fff6e0" },
8879
- sunIntensity: { default: 1 },
9096
+ sunIntensity: {
9097
+ default: 1,
9098
+ range: { min: 0 }
9099
+ },
8880
9100
  terrain: {
8881
9101
  default: "",
8882
9102
  nodePath: true
@@ -9497,7 +9717,13 @@ var Trail3D = class extends Node3D {
9497
9717
  static props = {
9498
9718
  width: { default: .3 },
9499
9719
  color: { default: "#ffffff" },
9500
- opacity: { default: .8 },
9720
+ opacity: {
9721
+ default: .8,
9722
+ range: {
9723
+ min: 0,
9724
+ max: 1
9725
+ }
9726
+ },
9501
9727
  seconds: { default: .6 },
9502
9728
  emitting: { default: true },
9503
9729
  additive: { default: false },
@@ -9546,6 +9772,7 @@ var Trail3D = class extends Node3D {
9546
9772
  kind: "trail",
9547
9773
  name: this.color,
9548
9774
  from: this.getPath(),
9775
+ wiredAs: this.wiringPath(),
9549
9776
  amount: this.seconds,
9550
9777
  at: engine.time
9551
9778
  });
@@ -11327,7 +11554,13 @@ var Tree3D = class Tree3D extends Node3D {
11327
11554
  area: { default: [10, 10] },
11328
11555
  trunkColor: { default: "#7a5a3a" },
11329
11556
  canopyColor: { default: "#4a7c3f" },
11330
- leafTexture: { default: "" },
11557
+ leafTexture: {
11558
+ default: "",
11559
+ asset: {
11560
+ kind: "texture",
11561
+ urlOnly: true
11562
+ }
11563
+ },
11331
11564
  leafFadeStart: { default: 0 },
11332
11565
  leafFadeEnd: { default: 0 },
11333
11566
  leafShadows: { default: true },
@@ -12661,9 +12894,25 @@ var VoxelGrid3D = class extends Node3D {
12661
12894
  static typeName = "VoxelGrid3D";
12662
12895
  static signals = ["blocksChanged"];
12663
12896
  static props = { voxels: { default: [] } };
12897
+ /**
12898
+ * Hard-fail an authored grid that cannot all be DRAWN.
12899
+ *
12900
+ * The draw loop stops at `MAX_INSTANCES` and the excess blocks stay in the
12901
+ * map — so `tileAt()` still reports them and a chunk collider builder still
12902
+ * makes them solid. The world past the cap was invisible AND you walked into
12903
+ * it. Every other 3D node whose instance count comes from authored data
12904
+ * hard-fails at load; this one silently truncated.
12905
+ */
12906
+ static validateJson(node) {
12907
+ const self = node;
12908
+ for (const [i, row] of self.voxels.entries()) if (!Array.isArray(row) || row.length < 3 || row.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `VoxelGrid3D '${node.name}': voxels[${i}] must be [x, y, z, tile?] (numbers), got ${JSON.stringify(row)}.`);
12909
+ if (self.voxels.length > MAX_INSTANCES) throw new IncantoError("BAD_FORMAT", `VoxelGrid3D '${node.name}': ${self.voxels.length} voxels exceeds the ${MAX_INSTANCES} this node can draw. Everything past the cap would still be SOLID and still be reported by tileAt(), and simply not appear. Split the world into several VoxelGrid3D nodes (one per chunk) and show the ones near the player.`);
12910
+ }
12664
12911
  /** JSON-authored blocks as [x, y, z, tile] tuples (generated terrains bake these). */
12665
12912
  voxels = [];
12666
12913
  appliedVoxels = null;
12914
+ /** Said once per node: a runtime overflow must not spam a frame loop. */
12915
+ reportedOverflow = false;
12667
12916
  map = /* @__PURE__ */ new Map();
12668
12917
  mesh = null;
12669
12918
  dirty = false;
@@ -12729,6 +12978,10 @@ var VoxelGrid3D = class extends Node3D {
12729
12978
  tile: t[3] ?? 0
12730
12979
  })));
12731
12980
  }
12981
+ if (this.map.size > MAX_INSTANCES && !this.reportedOverflow) {
12982
+ this.reportedOverflow = true;
12983
+ diagnose(this.tree?.engine ?? null, "error", `[incanto] VoxelGrid3D '${this.getPath()}' holds ${this.map.size} blocks and can draw ${MAX_INSTANCES}. The rest are still solid and still reported by tileAt(), and do not appear. Split the world across several VoxelGrid3D nodes.`);
12984
+ }
12732
12985
  if (!this.dirty || !this.mesh) return;
12733
12986
  this.dirty = false;
12734
12987
  const m4 = new Matrix4();
@@ -177,7 +177,7 @@ const pct = (v) => `${(v * 100).toFixed(1)}%`;
177
177
  /** The report a person or an agent reads. Verdict first, numbers after. */
178
178
  function frameText(stats) {
179
179
  const lines = [];
180
- if (stats.hidden) lines.push("THE PAGE IS HIDDEN a browser stops animating a window covered by another, so these pixels are whatever it last drew. Bring it to the front and capture again.");
180
+ if (stats.hidden) lines.push("the page is hidden, so the game is PAUSED these pixels were rendered for this capture (current), but nothing is moving. A `--do` drive needs the window in front.");
181
181
  if (stats.black) lines.push("BLACK SCREEN — nothing was drawn (no light, no camera, or nothing in view)");
182
182
  else if (stats.uniform) lines.push("one flat colour — the camera may be inside geometry, or only the sky is drawn");
183
183
  else if (!stats.subject) lines.push("nothing but background — the camera is pointed away from everything in the scene");