incanto 0.60.0 → 0.62.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 (68) hide show
  1. package/bin/incanto-check.mjs +13 -0
  2. package/bin/incanto-verify.mjs +134 -55
  3. package/dist/2d.d.ts +64 -3
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +5 -5
  6. package/dist/3d.js +5 -4
  7. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  8. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  9. package/dist/{create-game-ClnIb_M5.js → create-game-BpunnGPX.js} +78 -13
  10. package/dist/{create-game-BCm38FJV.js → create-game-Caut3bqN.js} +21 -294
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/debug.js +1 -1
  13. package/dist/{duplicate-DJQd44CD.js → duplicate-E4FUs5Bn.js} +1 -1
  14. package/dist/editor.js +41 -19
  15. package/dist/{environment-presets-8cjF3t6w.js → environment-presets-BAWeOeqf.js} +50 -13
  16. package/dist/{gameplay-BBEjPFsR.js → gameplay-CaHqDiQD.js} +177 -24
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +97 -6
  20. package/dist/index.js +7 -7
  21. package/dist/{loader-D8n7TU8W.js → loader-DEe272nY.js} +900 -2
  22. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  23. package/dist/net.d.ts +2 -2
  24. package/dist/net.js +1 -1
  25. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  26. package/dist/{physics-2d-DqdVp1bt.js → physics-2d-BXmu2i7W.js} +11 -3
  27. package/dist/{physics-3d-BP0DZb_1.js → physics-3d-ClxP6Uv7.js} +121 -17
  28. package/dist/react.d.ts +1 -1
  29. package/dist/react.js +1 -1
  30. package/dist/{register-BSu2dWGC.js → register-CNh4FlbD.js} +104 -16
  31. package/dist/{register-Da3hXh2H.js → register-D3yx8D4r.js} +228 -16
  32. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  33. package/dist/{replay-BCMK_VRP.d.ts → replay-C5x2vPF5.d.ts} +2 -2
  34. package/dist/{replay-BlNuIDdg.js → replay-DlgHItNv.js} +57 -197
  35. package/dist/{split-screen-CL5Yvxse.js → split-screen-CBM9wcMX.js} +3 -3
  36. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  37. package/dist/{src-DFpXBMJN.js → src-CH00_JsR.js} +1 -1
  38. package/dist/{teardown-CTTwhWSe.js → teardown-C7qP-dcC.js} +1 -1
  39. package/dist/{test-BeZ95pqw.js → test-BMkg8zMV.js} +29 -16
  40. package/dist/test.d.ts +45 -4
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +2 -2
  43. package/editor/assets/{agent8-DSJries_.js → agent8-m5mtAO_A.js} +1 -1
  44. package/editor/assets/{debug-D15Wi5TO.js → debug-CPhzCT8f.js} +1 -1
  45. package/editor/assets/{index-BjC88k97.js → index-D422P4kW.js} +92 -92
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +91 -0
  49. package/skills/incanto-3d-models.md +1 -1
  50. package/skills/incanto-assets.md +10 -1
  51. package/skills/incanto-audio.md +21 -13
  52. package/skills/incanto-behaviors-and-scripts.md +44 -3
  53. package/skills/incanto-building-2d-games.md +32 -4
  54. package/skills/incanto-building-3d-games.md +1 -1
  55. package/skills/incanto-editor.md +1 -1
  56. package/skills/incanto-gameplay-behaviors.md +39 -2
  57. package/skills/incanto-hud.md +5 -3
  58. package/skills/incanto-localization.md +7 -0
  59. package/skills/incanto-node-reference.md +12 -0
  60. package/skills/incanto-physics-and-input.md +23 -2
  61. package/skills/incanto-scene-json-authoring.md +6 -1
  62. package/skills/incanto-verifying-your-game.md +31 -1
  63. package/templates-app/beacon-isle-3d/package.json +1 -1
  64. package/templates-app/platformer-2d/package.json +1 -1
  65. package/templates-app/star-survivor/package.json +1 -1
  66. package/templates-app/tps-3d/package.json +1 -1
  67. package/templates-app/village-quest-3d/package.json +1 -1
  68. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
package/dist/editor.js CHANGED
@@ -1111,6 +1111,13 @@ var kt = {
1111
1111
  en: "Set waitTime (seconds), optionally autostart or oneShot, and connect its timeout signal to any handler. Because it is a node, the whole timing setup lives in the scene file — agents can read and tune it.",
1112
1112
  ko: "waitTime(초)을 정하고 autostart·oneShot을 선택한 뒤 timeout 시그널을 핸들러에 연결하세요. 노드이기 때문에 타이밍 설정 전체가 씬 파일에 남아 에이전트가 읽고 조정할 수 있습니다."
1113
1113
  }),
1114
+ L("Respawn", {
1115
+ en: "Catches whatever it hangs off when it leaves the world, and puts it back.",
1116
+ ko: "부모가 월드 밖으로 떨어지면 붙잡아 원래 자리로 되돌립니다."
1117
+ }, {
1118
+ en: "Make it a child of the player. With no props it catches its parent 50 m under the spawn (1000 px in 2D — the same line incanto-playtest calls \"fell\"), returns it to where it started and zeroes the velocity the fall built up. below names the line, to names the landing, target picks a different node. It emits respawned, and deliberately does not decide what falling COSTS — wire that to a ScoreKeeper or a Health if it should hurt.",
1119
+ ko: "플레이어의 자식으로 두세요. 프롭 없이도 스폰 50 m 아래(2D는 1000 px — incanto-playtest가 \"fell\"로 부르는 그 선)에서 부모를 붙잡아 처음 위치로 되돌리고, 낙하로 쌓인 속도를 0으로 만듭니다. below는 그 선, to는 착지 지점, target은 다른 노드를 지정합니다. respawned 시그널을 쏘며, 추락의 \"대가\"는 일부러 정하지 않습니다 — 피해를 주려면 ScoreKeeper나 Health에 연결하세요."
1120
+ }),
1114
1121
  L("HudLayer", {
1115
1122
  en: "Screen-space HUD overlay above the canvas — parent for UiText/UiBar/UiBanner.",
1116
1123
  ko: "캔버스 위 화면 고정 HUD 오버레이 — UiText/UiBar/UiBanner의 부모."
@@ -2372,7 +2379,8 @@ function Bn(e, t, n) {
2372
2379
  var Vn = [
2373
2380
  "connections",
2374
2381
  "input",
2375
- "multiplayer"
2382
+ "multiplayer",
2383
+ "strings"
2376
2384
  ];
2377
2385
  function Hn(e, t, n) {
2378
2386
  e.appendChild(Tr("scene")), e.appendChild(Ar("name", String(t.working.name ?? ""), (e) => {
@@ -9291,8 +9299,15 @@ function yc(e) {
9291
9299
  let t = e.assets;
9292
9300
  if (t) for (let e of Object.values(t)) typeof e?.url == "string" && !e.url.includes("/") && !e.url.includes(".") && (e.url = vc());
9293
9301
  }
9302
+ //#endregion
9303
+ //#region src/main.ts
9304
+ var bc = new Set([
9305
+ "Terrain3D",
9306
+ "Water3D",
9307
+ "River3D"
9308
+ ]);
9294
9309
  m(), b(), k(), ee(), A();
9295
- async function bc(e = {}) {
9310
+ async function xc(e = {}) {
9296
9311
  let t = e.api ?? ze, n = [], r = document.createElement("style");
9297
9312
  r.dataset.incantoEditor = "true", r.textContent = vs, document.head.appendChild(r), n.push(() => r.remove());
9298
9313
  let i = document.createElement("div");
@@ -9389,11 +9404,18 @@ async function bc(e = {}) {
9389
9404
  0,
9390
9405
  0
9391
9406
  ]),
9392
- onNodeScaled3D: (e, t) => de(e, "scale", t, [
9393
- 1,
9394
- 1,
9395
- 1
9396
- ]),
9407
+ onNodeScaled3D: (e, t) => {
9408
+ let n = u.nodeAt(e);
9409
+ if (n && bc.has(String(n.type))) {
9410
+ O(`${n.type} '${n.name}' cannot be scaled — scale would move only the mesh. Resize it with its "size" prop in the inspector.`);
9411
+ return;
9412
+ }
9413
+ de(e, "scale", t, [
9414
+ 1,
9415
+ 1,
9416
+ 1
9417
+ ]);
9418
+ },
9397
9419
  onNodeRotated2D: (e, t) => ue(e, "rotation", t, 0),
9398
9420
  onNodeRotated3D: (e, t) => de(e, "rotation", t, [
9399
9421
  0,
@@ -9479,15 +9501,15 @@ async function bc(e = {}) {
9479
9501
  ...e.shelf ? { shelf: e.shelf } : {},
9480
9502
  ...e.assetType ? { assetType: e.assetType } : {},
9481
9503
  ...e.onProps ? { onProps: e.onProps } : {},
9482
- ...e.asAsset ? { onAsset: (e, t, n) => wc(u, n, t) } : {}
9504
+ ...e.asAsset ? { onAsset: (e, t, n) => Tc(u, n, t) } : {}
9483
9505
  }) : void 0, le = !1, F = () => {
9484
9506
  if (!E) return;
9485
9507
  js(d, u), rt(a("#asset-tree"), u), f.textContent = "", it(f, u, Ce, ce) || Bn(f, u, {
9486
- modelRefs: () => Sc(u, "model"),
9508
+ modelRefs: () => Cc(u, "model"),
9487
9509
  animationsForSelection: () => u.selection ? P.modelAnimationsAt(u.selection) : [],
9488
9510
  bonesForSelection: (e) => u.selection ? P.boneNamesAt(u.selection, e) : [],
9489
- assetRefs: (e) => Cc(u, e),
9490
- addAsset: (e, t) => wc(u, e, t),
9511
+ assetRefs: (e) => wc(u, e),
9512
+ addAsset: (e, t) => Tc(u, e, t),
9491
9513
  ...ce ? { openLibrary: ce } : {},
9492
9514
  confirm: Ce,
9493
9515
  paint: {
@@ -9599,7 +9621,7 @@ async function bc(e = {}) {
9599
9621
  if (e.length === 0) {
9600
9622
  let e = u.working.root;
9601
9623
  if (!e) return;
9602
- let t = xc(e);
9624
+ let t = Sc(e);
9603
9625
  Ce(`This deletes the ROOT '${String(e.name ?? "")}'${t > 0 ? ` AND its ${t} descendant node${t === 1 ? "" : "s"}` : ""} — the scene goes empty, and the next node you add becomes the new root.`, "delete root", () => u.deleteRoot());
9604
9626
  return;
9605
9627
  }
@@ -9607,7 +9629,7 @@ async function bc(e = {}) {
9607
9629
  if (n.length === 0) {
9608
9630
  let n = e.reduce((e, t) => {
9609
9631
  let n = u.nodeAt(t);
9610
- return e + (n ? xc(n) : 0);
9632
+ return e + (n ? Sc(n) : 0);
9611
9633
  }, 0);
9612
9634
  if (n > 0) {
9613
9635
  Ce(`This deletes ${e.length > 1 ? `${e.length} nodes` : `'${String(u.nodeAt(e[0] ?? [])?.name ?? "")}'`} AND ${n} descendant node${n === 1 ? "" : "s"}.`, `delete ${e.length + n} nodes`, () => {
@@ -9783,20 +9805,20 @@ async function bc(e = {}) {
9783
9805
  }
9784
9806
  };
9785
9807
  }
9786
- function xc(e) {
9808
+ function Sc(e) {
9787
9809
  let t = 0;
9788
- for (let n of e.children ?? []) t += 1 + xc(n);
9810
+ for (let n of e.children ?? []) t += 1 + Sc(n);
9789
9811
  return t;
9790
9812
  }
9791
- function Sc(e, t) {
9813
+ function Cc(e, t) {
9792
9814
  let n = e.working.assets;
9793
9815
  return n ? Object.entries(n).filter(([, e]) => e?.type === t).map(([e]) => `$${e}`) : [];
9794
9816
  }
9795
- function Cc(e, t) {
9817
+ function wc(e, t) {
9796
9818
  let n = e.working.assets;
9797
9819
  return n ? Object.entries(n).filter(([, e]) => !t || e?.type !== void 0 && t.includes(e.type)).map(([e]) => `$${e}`) : [];
9798
9820
  }
9799
- function wc(e, t, n) {
9821
+ function Tc(e, t, n) {
9800
9822
  let r = e.working.assets ?? {};
9801
9823
  for (let [e, t] of Object.entries(r)) if (t?.url === n.url) return `$${e}`;
9802
9824
  let i = t;
@@ -9806,4 +9828,4 @@ function wc(e, t, n) {
9806
9828
  }), `$${i}`;
9807
9829
  }
9808
9830
  //#endregion
9809
- export { N as formatSceneJson, ze as httpEditorApi, bc as mountEditor };
9831
+ export { N as formatSceneJson, ze as httpEditorApi, xc as mountEditor };
@@ -1,11 +1,11 @@
1
- import { w as diagnose } from "./loader-D8n7TU8W.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-Da3hXh2H.js";
1
+ import { k as diagnose } from "./loader-DEe272nY.js";
2
+ import { L as translateOn, t as registerCoreNodes } from "./register-D3yx8D4r.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
5
+ import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
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 StaticBody3D, F as WaterCutout3D, G as colliderFootDrop, I as Area3D, L as CharacterBody3D, N as Water3D, R as PhysicsBody3D, V as Node3D, w as tolerateUnknownAction, z as RigidBody3D } from "./gameplay-BBEjPFsR.js";
8
+ import { B as CharacterBody3D, F as Water3D, H as RigidBody3D, J as colliderFootDrop, L as WaterCutout3D, R as rejectScale, U as StaticBody3D, V as PhysicsBody3D, W as Node3D, w as tolerateUnknownAction, z as Area3D } from "./gameplay-CaHqDiQD.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.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";
@@ -727,7 +727,8 @@ var Joint3D = class extends Node3D {
727
727
  },
728
728
  target: {
729
729
  default: "",
730
- nodePath: true
730
+ nodePath: true,
731
+ required: true
731
732
  },
732
733
  anchor: { default: [
733
734
  0,
@@ -1574,6 +1575,7 @@ var Terrain3D = class extends Node3D {
1574
1575
  }
1575
1576
  /** Loader hook: bad themes/layers/grids fail at LOAD, not at render. */
1576
1577
  static validateJson(node) {
1578
+ rejectScale(node, "heightAt(), the heightfield collider, drape and terrain-nav all");
1577
1579
  const t = node;
1578
1580
  if (!TERRAIN_THEMES.includes(t.theme)) throw new IncantoError("BAD_FORMAT", `Terrain3D '${node.name}' theme must be one of [${TERRAIN_THEMES.join(", ")}], got '${t.theme}'.`, {
1579
1581
  prop: "theme",
@@ -2335,7 +2337,8 @@ var BoneAttachment3D = class extends Node3D {
2335
2337
  /** Node path to the ModelInstance3D whose skeleton to ride. */
2336
2338
  target: {
2337
2339
  default: "",
2338
- nodePath: true
2340
+ nodePath: true,
2341
+ required: true
2339
2342
  },
2340
2343
  /** Bone name ("RightHand", "Head", …). Empty = inert. */
2341
2344
  bone: { default: "" }
@@ -2428,7 +2431,8 @@ var BoneLookAt3D = class extends Node3D {
2428
2431
  /** Node path to the ModelInstance3D whose bone to steer. */
2429
2432
  target: {
2430
2433
  default: "",
2431
- nodePath: true
2434
+ nodePath: true,
2435
+ required: true
2432
2436
  },
2433
2437
  /** Bone name ("Head", "Neck", "Spine2" — Mixamo spellings matched). */
2434
2438
  bone: { default: "Head" },
@@ -2440,7 +2444,13 @@ var BoneLookAt3D = class extends Node3D {
2440
2444
  /** Comfort cone half-angle, degrees — beyond it the head returns to the anim. */
2441
2445
  maxAngleDeg: { default: 75 },
2442
2446
  /** Blend weight 0..1 (how far toward the target the bone turns). */
2443
- weight: { default: .85 },
2447
+ weight: {
2448
+ default: .85,
2449
+ range: {
2450
+ min: 0,
2451
+ max: 1
2452
+ }
2453
+ },
2444
2454
  /** The bone's local facing axis. */
2445
2455
  forwardAxis: {
2446
2456
  default: "+z",
@@ -6449,9 +6459,21 @@ var ModelInstance3D = class extends Node3D {
6449
6459
  static signals = ["animationFinished"];
6450
6460
  static props = {
6451
6461
  ...Node3D.props,
6452
- model: { default: "" },
6453
- animation: { default: "" },
6454
- targetHeight: { default: 0 },
6462
+ model: {
6463
+ default: "",
6464
+ asset: {
6465
+ kind: "model",
6466
+ refOnly: true
6467
+ }
6468
+ },
6469
+ animation: {
6470
+ default: "",
6471
+ asset: { kind: "animation" }
6472
+ },
6473
+ targetHeight: {
6474
+ default: 0,
6475
+ range: { min: 0 }
6476
+ },
6455
6477
  castShadow: { default: false },
6456
6478
  animationLoop: { default: true },
6457
6479
  /** Playback rate: 1 = authored speed, 0.5 = half, 2 = double. */
@@ -6466,8 +6488,22 @@ var ModelInstance3D = class extends Node3D {
6466
6488
  upperBodyRoot: { default: "Spine" },
6467
6489
  receiveShadow: { default: false },
6468
6490
  tint: { default: "" },
6469
- metalness: { default: -1 },
6470
- roughness: { default: -1 }
6491
+ metalness: {
6492
+ default: -1,
6493
+ range: {
6494
+ min: 0,
6495
+ max: 1,
6496
+ allow: [-1]
6497
+ }
6498
+ },
6499
+ roughness: {
6500
+ default: -1,
6501
+ range: {
6502
+ min: 0,
6503
+ max: 1,
6504
+ allow: [-1]
6505
+ }
6506
+ }
6471
6507
  };
6472
6508
  model = "";
6473
6509
  animation = "";
@@ -8770,6 +8806,7 @@ var River3D = class River3D extends Node3D {
8770
8806
  _bodies = [];
8771
8807
  /** Loader hook: a malformed river fails at LOAD, not as an invisible ribbon. */
8772
8808
  static validateJson(node) {
8809
+ rejectScale(node, "the flow query and its collider both");
8773
8810
  const r = node;
8774
8811
  if (!Array.isArray(r.path)) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path must be an array of [x, z] points, got ${JSON.stringify(r.path)}.`, { prop: "path" });
8775
8812
  if (r.path.length > 0 && r.path.length < 2) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path needs at least 2 points (source → mouth), got ${r.path.length}.`, { prop: "path" });
@@ -1,7 +1,7 @@
1
- import { C as Node, E as Behavior, b as effectiveOrder, j as registerBehavior, n as loadScene, p as SaveSlots, w as diagnose } from "./loader-D8n7TU8W.js";
1
+ import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-DEe272nY.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-CwwhxQgb.js";
4
- import { t as duplicateNode } from "./duplicate-DJQd44CD.js";
4
+ import { t as duplicateNode } from "./duplicate-E4FUs5Bn.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();
@@ -2625,6 +2625,51 @@ var CharacterBody3D = class extends PhysicsBody3D {
2625
2625
  }
2626
2626
  };
2627
2627
  //#endregion
2628
+ //#region src/3d/nodes/unscalable.ts
2629
+ /**
2630
+ * Refuse `scale` on a node that keeps a CPU twin of itself.
2631
+ *
2632
+ * `scale` is a plain `Node3D` prop and the 3D skill actively recommends it —
2633
+ * *"resize with `scale`, not `size` — resizing a mesh rebuilds its geometry
2634
+ * every call, scaling is free"*. On a mesh that is true. On `Terrain3D`,
2635
+ * `Water3D` and `River3D` it resizes only the PICTURE: everything the gameplay
2636
+ * layer reads is scale-blind, because it reads `size`.
2637
+ *
2638
+ * - `Terrain3D.heightAt` indexes a heightmap built from `size` and composes
2639
+ * ancestor POSITIONS only
2640
+ * - `buildHeightfieldDesc3D` passes `{x: hm.width, y: 1, z: hm.depth}` with no
2641
+ * scale term
2642
+ * - `Water3D._waterYAt` rejects anything outside `size / 2` of the node
2643
+ *
2644
+ * Measured on `size: [100, 100]` at `scale: [2, 1, 2]`:
2645
+ *
2646
+ * ```
2647
+ * terrain rendered X extent = [-100.0, 100.0] (was [-50, 50])
2648
+ * terrain heightAt(70, 0) = -50.000 (unchanged: the island floor)
2649
+ * water rendered X extent = [-40.0, 40.0]
2650
+ * water _waterYAt(30, 0) = null (unchanged)
2651
+ * ```
2652
+ *
2653
+ * So the player walks off an invisible cliff at ±50 m onto ground that is
2654
+ * visibly there for another 50; drape, spawn placement and terrain-nav all
2655
+ * agree with the collider and disagree with the picture; a scaled water's
2656
+ * underwater fog, splash and `avoidWater` culling stop at half the visible
2657
+ * shoreline. `incanto check` called the scene valid and `auditScene` said
2658
+ * nothing.
2659
+ *
2660
+ * A hard load error rather than a warning, per the engine's own guardrail: the
2661
+ * dial the author wanted exists and is one word away.
2662
+ */
2663
+ function rejectScale(node, reads) {
2664
+ const scale = node.scale;
2665
+ if (!Array.isArray(scale)) return;
2666
+ if (scale.every((v) => Math.abs((v ?? 1) - 1) < 1e-4)) return;
2667
+ const type = node.constructor.typeName ?? "node";
2668
+ const size = node.size;
2669
+ const suggestion = Array.isArray(size) ? `size: [${size.map((v, i) => (v ?? 0) * (scale[i === 1 ? 2 : i] ?? 1)).join(", ")}]` : "size";
2670
+ throw new IncantoError("BAD_FORMAT", `${type} '${node.name}' has scale ${JSON.stringify(scale)}. On this node scale resizes only the MESH — ${reads} read "size" — so the ground you can see would reach past the ground you can stand on. Resize with "${suggestion}" instead.`, { prop: "scale" });
2671
+ }
2672
+ //#endregion
2628
2673
  //#region src/3d/nodes/water-cutout-3d.ts
2629
2674
  /**
2630
2675
  * A box the water does not enter.
@@ -3155,6 +3200,7 @@ var Water3D = class Water3D extends Node3D {
3155
3200
  }
3156
3201
  /** Loader hook: degenerate planes and bad enums fail at LOAD, not at render. */
3157
3202
  static validateJson(node) {
3203
+ rejectScale(node, "the water surface query, underwater fog, splashes and buoyancy all");
3158
3204
  const w = node;
3159
3205
  if (!((w.size[0] ?? 0) > 0) || !((w.size[1] ?? 0) > 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' size must be positive [width, depth] meters, got ${JSON.stringify(w.size)}.`, { prop: "size" });
3160
3206
  if (!WATER_QUALITIES.includes(w.quality)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' quality must be one of [${WATER_QUALITIES.join(", ")}], got '${w.quality}'.`, {
@@ -4388,6 +4434,56 @@ function smoothingFactor(smoothing, dt) {
4388
4434
  const s = Math.min(Math.max(smoothing, 0), .99);
4389
4435
  return s <= 0 ? 1 : 1 - s ** (dt * 60);
4390
4436
  }
4437
+ /**
4438
+ * A node's position with every spatial ancestor's added in.
4439
+ *
4440
+ * The tree composes transforms for DRAWING — three does it for free — and every
4441
+ * piece of engine logic that reads `node.position` gets the LOCAL number
4442
+ * instead. Physics already knew (`worldPosition2D`/`worldPosition3D` in the
4443
+ * adapters); the camera did not, and neither did anything that chases a target.
4444
+ *
4445
+ * A camera parented to the player is the Godot and Phaser idiom, and the
4446
+ * authoring skill explicitly permits it — *"EVERY node type can hold children …
4447
+ * any node works as a grouping container"*. Measured on exactly that scene:
4448
+ *
4449
+ * ```
4450
+ * player world position : [1400, 900]
4451
+ * renderer view centre : {"x":0,"y":0}
4452
+ * framing : camera /Level/Player/Cam centred [1400, 900]
4453
+ * 1 in view, 0 outside it
4454
+ * ```
4455
+ *
4456
+ * The renderer drew empty world 1600 px from the player while `framing` — the
4457
+ * instrument the skills tell you to trust before handing a game back —
4458
+ * certified the player on screen.
4459
+ *
4460
+ * Arity comes from the node itself: a 2D node has two components, a 3D node
4461
+ * three, and an ancestor that is neither contributes nothing.
4462
+ */
4463
+ function worldPosition(node) {
4464
+ const own = node.position;
4465
+ if (!Array.isArray(own)) return [];
4466
+ const out = own.slice();
4467
+ for (let at = node.parent; at; at = at.parent) {
4468
+ const p = at.position;
4469
+ if (!Array.isArray(p)) continue;
4470
+ for (let i = 0; i < out.length; i++) out[i] = (out[i] ?? 0) + (p[i] ?? 0);
4471
+ }
4472
+ return out;
4473
+ }
4474
+ /**
4475
+ * Turn a world position into the local one `node.position` has to hold for the
4476
+ * node to END UP there — the inverse of `worldPosition` for its parent chain.
4477
+ */
4478
+ function localFromWorld(node, world) {
4479
+ const out = world.slice();
4480
+ for (let at = node.parent; at; at = at.parent) {
4481
+ const p = at.position;
4482
+ if (!Array.isArray(p)) continue;
4483
+ for (let i = 0; i < out.length; i++) out[i] = (out[i] ?? 0) - (p[i] ?? 0);
4484
+ }
4485
+ return out;
4486
+ }
4391
4487
  //#endregion
4392
4488
  //#region src/gameplay/chase.ts
4393
4489
  /**
@@ -4975,6 +5071,7 @@ var FloatAway = class extends Behavior {
4975
5071
  onReady() {
4976
5072
  const node = this.node;
4977
5073
  this.start = [...node.position ?? []];
5074
+ if (node.opacity === void 0) diagnose(this.node.tree?.engine, "warn", `[incanto] FloatAway on '${this.node.getPath()}' will not fade — ${this.node.constructor.name} has no "opacity" prop, so it pops out at full brightness instead. Use a node that has one (Label, Label3D, Sprite2D, Sprite3D).`);
4978
5075
  }
4979
5076
  update(dt) {
4980
5077
  if (this.done) return;
@@ -5024,9 +5121,18 @@ var FollowCamera = class extends Behavior {
5024
5121
  /** Constant offset added to the target's position ([x,y] or [x,y,z]). */
5025
5122
  offset: { default: [] },
5026
5123
  /** 0..1 retention factor: 0 = snap, higher = smoother chase. */
5027
- smoothing: { default: 0 },
5124
+ smoothing: {
5125
+ default: 0,
5126
+ range: {
5127
+ min: 0,
5128
+ max: 1
5129
+ }
5130
+ },
5028
5131
  /** Don't move until the target is at least this far from the desired point. */
5029
- deadzone: { default: 0 }
5132
+ deadzone: {
5133
+ default: 0,
5134
+ range: { min: 0 }
5135
+ }
5030
5136
  };
5031
5137
  static signals = [];
5032
5138
  target = "";
@@ -5052,11 +5158,12 @@ var FollowCamera = class extends Behavior {
5052
5158
  if (!hasPosition$1(cam)) return;
5053
5159
  const target = cam.getNodeOrNull(this.target);
5054
5160
  if (!target || !hasPosition$1(target)) return;
5055
- const desired = add(target.position, this.offset);
5056
- if (!(this.deadzone > 0 && distance$1(cam.position, desired) <= this.deadzone)) {
5161
+ const here = worldPosition(cam);
5162
+ const desired = add(worldPosition(target), this.offset);
5163
+ if (!(this.deadzone > 0 && distance$1(here, desired) <= this.deadzone)) {
5057
5164
  const f = smoothingFactor(this.smoothing, dt);
5058
- if (f >= 1) cam.position = [...desired];
5059
- else cam.position = cam.position.map((c, i) => c + ((desired[i] ?? c) - c) * f);
5165
+ if (f >= 1) cam.position = localFromWorld(cam, desired);
5166
+ else cam.position = localFromWorld(cam, here.map((c, i) => c + ((desired[i] ?? c) - c) * f));
5060
5167
  }
5061
5168
  if (this.shakeT > 0) {
5062
5169
  this.shakeT -= dt;
@@ -5109,7 +5216,6 @@ function tolerateUnknownAction(ctx, action, read, fallback) {
5109
5216
  function restartScene(engine) {
5110
5217
  const source = engine.scene?.source;
5111
5218
  if (!source) return;
5112
- engine.timeScale = 1;
5113
5219
  engine.setScene(loadScene(jsonClone(source)));
5114
5220
  }
5115
5221
  /**
@@ -5119,7 +5225,6 @@ function restartScene(engine) {
5119
5225
  function goToScene(engine, sceneJson, opts) {
5120
5226
  const fade = opts?.fadeSeconds ?? .4;
5121
5227
  const swap = () => {
5122
- engine.timeScale = 1;
5123
5228
  engine.setScene(loadScene(jsonClone(sceneJson)));
5124
5229
  };
5125
5230
  if (typeof document === "undefined" || fade <= 0) {
@@ -5500,13 +5605,14 @@ function screenFlash(engine, color = "#ffffff", opacity = .35, seconds = .25) {
5500
5605
  */
5501
5606
  function hitStop(engine, seconds = .08) {
5502
5607
  recordEffect(engine, "hitstop", "", "engine", seconds);
5503
- const state = hitStops.get(engine);
5608
+ const state = liveHitStop(engine);
5504
5609
  if (state) {
5505
5610
  state.until = Math.max(state.until, engine.unscaledTime + seconds);
5506
5611
  return;
5507
5612
  }
5508
5613
  const restore = engine.timeScale;
5509
5614
  const entry = {
5615
+ startedAt: engine.unscaledTime,
5510
5616
  until: engine.unscaledTime + seconds,
5511
5617
  restore,
5512
5618
  release: () => {}
@@ -5514,12 +5620,35 @@ function hitStop(engine, seconds = .08) {
5514
5620
  hitStops.set(engine, entry);
5515
5621
  engine.timeScale = 0;
5516
5622
  entry.release = engine.updated.connect(() => {
5623
+ if (!liveHitStop(engine)) return;
5517
5624
  if (engine.unscaledTime < entry.until) return;
5518
5625
  engine.timeScale = entry.restore;
5519
5626
  entry.release();
5520
5627
  hitStops.delete(engine);
5521
5628
  });
5522
5629
  }
5630
+ /**
5631
+ * The engine's pending freeze, if the clock it was measured against still runs.
5632
+ *
5633
+ * `setScene` resets `unscaledTime` to 0, and a freeze deadline is an ABSOLUTE
5634
+ * reading of that clock — so a hit-stop taken on the killing blow, two minutes
5635
+ * into a level, survived the restart as a deadline two minutes in the future.
5636
+ * Measured: `hitStop(engine, 0.08)`, then the restart, then five real seconds
5637
+ * of ticks — 2.08 of them frozen, and a level that long freezes that long. The
5638
+ * quieter half is the next scene's OWN hit-stop: it found the stale entry,
5639
+ * extended a deadline nobody was waiting on, and never froze anything.
5640
+ *
5641
+ * A clock that reads earlier than the freeze began cannot be the clock the
5642
+ * freeze was scheduled on. `setScene` already put `timeScale` back to 1, so
5643
+ * there is nothing to restore — just let go.
5644
+ */
5645
+ function liveHitStop(engine) {
5646
+ const entry = hitStops.get(engine);
5647
+ if (!entry) return void 0;
5648
+ if (engine.unscaledTime >= entry.startedAt) return entry;
5649
+ entry.release();
5650
+ hitStops.delete(engine);
5651
+ }
5523
5652
  const hitStops = /* @__PURE__ */ new Map();
5524
5653
  //#endregion
5525
5654
  //#region src/gameplay/lifetime.ts
@@ -5605,7 +5734,10 @@ function ease(curve, t) {
5605
5734
  var MoveTo = class extends Behavior {
5606
5735
  static props = {
5607
5736
  /** Destination position ([x,y] or [x,y,z]). */
5608
- to: { default: [] },
5737
+ to: {
5738
+ default: [],
5739
+ required: true
5740
+ },
5609
5741
  /** Seconds the move takes. */
5610
5742
  duration: { default: 1 },
5611
5743
  /** Easing curve. */
@@ -5819,7 +5951,10 @@ function distanceBetween(a, b) {
5819
5951
  var Patrol = class extends Behavior {
5820
5952
  static props = {
5821
5953
  /** Waypoints: array of position arrays OR node-path strings to read. */
5822
- points: { default: [] },
5954
+ points: {
5955
+ default: [],
5956
+ required: true
5957
+ },
5823
5958
  /** Units per second along the path. */
5824
5959
  speed: { default: 60 },
5825
5960
  /** Wrap to the first point after the last (ignored in pingpong). */
@@ -6031,12 +6166,12 @@ var Projectile = class extends Behavior {
6031
6166
  return [Math.cos(rad), Math.sin(rad)];
6032
6167
  }
6033
6168
  if (Array.isArray(rot)) {
6034
- const yaw = (rot[1] ?? 0) * Math.PI / 180;
6035
- const pitch = (rot[0] ?? 0) * Math.PI / 180;
6169
+ const ry = (rot[1] ?? 0) * Math.PI / 180;
6170
+ const rx = (rot[0] ?? 0) * Math.PI / 180;
6036
6171
  return normalize([
6037
- -Math.sin(yaw) * Math.cos(pitch),
6038
- Math.sin(pitch),
6039
- -Math.cos(yaw) * Math.cos(pitch)
6172
+ Math.sin(ry),
6173
+ -Math.sin(rx) * Math.cos(ry),
6174
+ Math.cos(rx) * Math.cos(ry)
6040
6175
  ]);
6041
6176
  }
6042
6177
  return [1, 0];
@@ -6322,13 +6457,27 @@ var SpawnSource = class {
6322
6457
  * spawner's node), and return the clone.
6323
6458
  *
6324
6459
  * Placement: with no `at`, the clone keeps the template's authored position;
6325
- * with `at`, it spawns at the spawner's position plus `at` (a non-spatial
6326
- * spawner like a plain Node treats `at` as absolute).
6460
+ * with `at`, it ends up at the spawner's position plus `at`.
6461
+ *
6462
+ * `at` is written as the clone's LOCAL position, because the clone is added
6463
+ * as a CHILD of the spawner's node and that parent's transform already
6464
+ * contributes the spawner's own position. This used to add the two, so the
6465
+ * spawner's position was applied twice and the further from the origin you
6466
+ * put the nest, the further away its enemies appeared. Measured on a spawner
6467
+ * at [820, 240] with `"at": [0, -50]`:
6468
+ *
6469
+ * ```
6470
+ * expected world = [820, 190]
6471
+ * clone WORLD position = [1640, 430] error = [820, 240]
6472
+ * ```
6473
+ *
6474
+ * The scene loads, the spawner spawns, the clone is alive and visible — it is
6475
+ * just somewhere else in the level, and nothing has anything to report.
6327
6476
  */
6328
6477
  spawn(template, opts) {
6329
6478
  const clone = duplicateNode(template);
6330
6479
  const at = opts?.at;
6331
- if (at && at.length > 0 && hasPosition$1(clone)) clone.position = add(hasPosition$1(this.behavior.node) ? this.behavior.node.position : [], at);
6480
+ if (at && at.length > 0 && hasPosition$1(clone)) clone.position = add((opts?.parent ?? this.behavior.node) === this.behavior.node || !hasPosition$1(this.behavior.node) ? [] : this.behavior.node.position, at);
6332
6481
  if (typeof clone.visible === "boolean") clone.visible = true;
6333
6482
  (opts?.parent ?? this.behavior.node).addChild(clone);
6334
6483
  return clone;
@@ -6355,7 +6504,8 @@ var Spawner = class extends Behavior {
6355
6504
  /** Node path of the template to clone. */
6356
6505
  prefab: {
6357
6506
  default: "",
6358
- nodePath: true
6507
+ nodePath: true,
6508
+ required: true
6359
6509
  },
6360
6510
  /** Seconds between spawns. */
6361
6511
  interval: { default: 1 },
@@ -6508,7 +6658,10 @@ var Wander = class extends Behavior {
6508
6658
  var WaveSpawner = class extends Behavior {
6509
6659
  static props = {
6510
6660
  /** Array of { prefab, count, interval, delayBefore }. */
6511
- waves: { default: [] },
6661
+ waves: {
6662
+ default: [],
6663
+ required: true
6664
+ },
6512
6665
  /** Begin the first wave's countdown at ready. */
6513
6666
  autoStart: { default: true }
6514
6667
  };
@@ -6825,4 +6978,4 @@ function registerGameplayBehaviors(opts) {
6825
6978
  for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
6826
6979
  }
6827
6980
  //#endregion
6828
- export { Health as A, StaticBody3D as B, restartScene as C, DayNight as D, FloatAway as E, WaterCutout3D as F, colliderFootDrop as G, validateCollider3D as H, Area3D as I, CharacterBody3D as L, Chase as M, Water3D as N, phaseOf as O, WATER_CUTOUT_MAX as P, PhysicsBody3D as R, goToScene as S, FollowCamera as T, createCausticsQuad as U, Node3D as V, WATER_MAX_RIPPLES as W, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Collector as j, DamageOnContact as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, tolerateUnknownAction as w, GameFlow as x, screenFlash as y, RigidBody3D as z };
6981
+ export { Health as A, CharacterBody3D as B, restartScene as C, DayNight as D, FloatAway as E, Water3D as F, validateCollider3D as G, RigidBody3D as H, WATER_CUTOUT_MAX as I, colliderFootDrop as J, createCausticsQuad as K, WaterCutout3D as L, Chase as M, localFromWorld as N, phaseOf as O, worldPosition as P, rejectScale as R, goToScene as S, FollowCamera as T, StaticBody3D as U, PhysicsBody3D as V, Node3D as W, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Collector as j, DamageOnContact as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, WATER_MAX_RIPPLES as q, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, tolerateUnknownAction as w, GameFlow as x, screenFlash as y, Area3D as z };
@@ -1,4 +1,4 @@
1
- import { At as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-DWKTUzKI.js";
1
+ import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-DsgayMsH.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as Health, C as restartScene, D as DayNight, E as FloatAway, M as Chase, O as phaseOf, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Collector, k as DamageOnContact, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-BBEjPFsR.js";
1
+ import { A as Health, C as restartScene, D as DayNight, E as FloatAway, M as Chase, O as phaseOf, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Collector, k as DamageOnContact, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-CaHqDiQD.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };