incanto 0.74.0 → 0.75.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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +743 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-BAiA-FjP.js → create-game-DVUhHs2B.js} +79 -10
  10. package/dist/{create-game-ssvGvP7W.js → create-game-DnwhJ5nW.js} +83 -7
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-O8mxpgRX.d.ts} +1 -1
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-BP77Dl0K.js} +2005 -60
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-D-JuGura.js} +9309 -6595
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-x3bCrQf0.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-Bz2VPQ6D.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-Buita5T9.js} +2 -2
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  31. package/dist/{register-DRQyZKGU.js → register-DxAbTO2T.js} +191 -430
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-DLKVc6Hj.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-HlVUeCSn.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-BXlYM9rv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-S42XGP4Y.js} +1 -1
  45. package/editor/assets/index-DgrgUKno.js +11046 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1630 -299
  49. package/skills/incanto-3d-character.md +358 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +17 -6
  54. package/skills/incanto-environment.md +227 -2
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +393 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +151 -2
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -1,11 +1,11 @@
1
- import { Y as diagnose } from "./save-slots-CEUGeLuB.js";
2
- import { M as translateOn, t as registerCoreNodes } from "./register-CjociOtt.js";
1
+ import { et as diagnose, h as rebuildOptions, i as worldPosition, v as buildNodeJson } from "./pose-ByFB_J3O.js";
2
+ import { P as translateOn, t as registerCoreNodes } from "./register-B768Qedj.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-CDOMybym.js";
5
5
  import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-CioTZB-y.js";
6
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-Bz3DHD_4.js";
8
- import { E as tolerateUnknownAction, G as PhysicsBody3D, H as rejectScale, J as Node3D, K as RigidBody3D, O as effectRng, Q as colliderFootDrop, U as Area3D, V as WaterCutout3D, W as CharacterBody3D, et as worldPosition, q as StaticBody3D, z as Water3D } from "./gameplay-Bxe1sMVT.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-SQa5gIu2.js";
8
+ import { $ as Water3D, Q as tolerateUnknownAction, V as effectRng, at as PhysicsBody3D, ct as Node3D, ft as colliderFootDrop, ht as findHealth, it as CharacterBody3D, nt as rejectScale, ot as RigidBody3D, rt as Area3D, st as StaticBody3D, tt as WaterCutout3D } from "./gameplay-D-JuGura.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
10
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, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, SpotLight, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
@@ -752,9 +752,14 @@ const matScratch = new Matrix4();
752
752
  * "props": { "type": "spherical", "target": "%Anchor", "anchor": [0, -20] } }
753
753
  *
754
754
  * Types: `fixed` welds the bodies rigidly · `spherical` is a ball joint at the
755
- * anchors · `rope` caps the anchor distance at `length` (m; 0 = measured at
756
- * creation) · `spring` pulls toward `length` with `stiffness`/`damping`.
757
- * Anchors are LOCAL meter offsets on each body.
755
+ * anchors · `revolute` is a HINGE about `axis` (a door, a wheel, a lever) with
756
+ * optional `limits` (degrees) and a `motorSpeed` (degrees a second) ·
757
+ * `prismatic` is a SLIDE along `axis` (a portcullis, an elevator, a piston)
758
+ * with `limits` in metres, a `motorSpeed` in metres a second and `travel`
759
+ * read back · `rope`
760
+ * caps the anchor distance at `length` (m; 0 = measured at creation) ·
761
+ * `spring` pulls toward `length` with `stiffness`/`damping`. Anchors are LOCAL
762
+ * meter offsets on each body; `angle` reads the hinge back.
758
763
  */
759
764
  var Joint3D = class extends Node3D {
760
765
  static typeName = "Joint3D";
@@ -764,6 +769,8 @@ var Joint3D = class extends Node3D {
764
769
  options: [
765
770
  "fixed",
766
771
  "spherical",
772
+ "revolute",
773
+ "prismatic",
767
774
  "rope",
768
775
  "spring"
769
776
  ]
@@ -783,6 +790,21 @@ var Joint3D = class extends Node3D {
783
790
  0,
784
791
  0
785
792
  ] },
793
+ /** The hinge axis of a `revolute`, or the line of a `prismatic`, in this body's local frame. */
794
+ axis: { default: [
795
+ 0,
796
+ 1,
797
+ 0
798
+ ] },
799
+ /** The stops: `[minDeg, maxDeg]` for a `revolute`, `[min, max]` metres for a `prismatic`; empty = free. */
800
+ limits: { default: [] },
801
+ /** The motor's target speed — degrees a second (`revolute`) or metres a second (`prismatic`); 0 = no motor. */
802
+ motorSpeed: { default: 0 },
803
+ /** How hard the motor holds its speed (acceleration per rad/s of error). */
804
+ motorStrength: {
805
+ default: 20,
806
+ range: { min: 0 }
807
+ },
786
808
  length: { default: 0 },
787
809
  stiffness: { default: 50 },
788
810
  damping: { default: 5 },
@@ -792,6 +814,33 @@ var Joint3D = class extends Node3D {
792
814
  }
793
815
  };
794
816
  type = "spherical";
817
+ /** Hinge axis of a `revolute`, or the line of a `prismatic` (local to this body). */
818
+ axis = [
819
+ 0,
820
+ 1,
821
+ 0
822
+ ];
823
+ /**
824
+ * `[min, max]` in degrees for a `revolute` swing; `[]` = no stops. Measured
825
+ * as `angle` is: this body turned about `axis`, relative to the target.
826
+ */
827
+ limits = [];
828
+ /** Degrees a second the motor drives the hinge at; `0` = no motor (free). */
829
+ motorSpeed = 0;
830
+ /** The motor's gain — how quickly it reaches and holds `motorSpeed`. */
831
+ motorStrength = 20;
832
+ /**
833
+ * The hinge's current angle in degrees — this body's rotation about `axis`
834
+ * relative to the target, written by physics every step. `0` when the joint
835
+ * is not a `revolute` or is not simulated yet.
836
+ */
837
+ angle = 0;
838
+ /**
839
+ * A `prismatic` slide's current travel in metres — this body's anchor along
840
+ * `axis` relative to the target's, written by physics every step. `0` where
841
+ * the two anchors coincide, which is where the scene loaded it.
842
+ */
843
+ travel = 0;
795
844
  target = "";
796
845
  anchor = [
797
846
  0,
@@ -835,6 +884,11 @@ var Joint3D = class extends Node3D {
835
884
  onEnterTree() {
836
885
  if (!(this.parent instanceof PhysicsBody3D)) throw new IncantoError("TREE_VIOLATION", `Joint3D '${this.name}' must be a child of a physics body (its body A); parent is '${this.parent?.name ?? "none"}'.`);
837
886
  if (this.target === "") throw new IncantoError("BAD_FORMAT", `Joint3D '${this.name}' needs a "target" node path to the body it links to.`);
887
+ if (this.type === "revolute" || this.type === "prismatic") {
888
+ const [x, y, z] = this.axis;
889
+ if (!(Math.hypot(x ?? 0, y ?? 0, z ?? 0) > 1e-6)) throw new IncantoError("BAD_FORMAT", `Joint3D '${this.name}': a ${this.type} joint needs a non-zero "axis" (got ${JSON.stringify(this.axis)}).`);
890
+ if (this.limits.length !== 0 && this.limits.length !== 2) throw new IncantoError("BAD_FORMAT", `Joint3D '${this.name}': "limits" is [minDeg, maxDeg] or [] (got ${JSON.stringify(this.limits)}).`);
891
+ }
838
892
  }
839
893
  /** @internal The two bodies, resolved (throws on a bad target at step time). */
840
894
  _resolveBodies() {
@@ -852,6 +906,25 @@ var Joint3D = class extends Node3D {
852
906
  }
853
907
  };
854
908
  //#endregion
909
+ //#region src/3d/terrain/course.ts
910
+ /** Is this a point either spelling accepts? */
911
+ function isCoursePoint(p) {
912
+ if (!Array.isArray(p)) return false;
913
+ if (p.length === 2) return Number.isFinite(p[0]) && Number.isFinite(p[1]);
914
+ if (p.length === 3) return Number.isFinite(p[0]) && Number.isFinite(p[1]) && Number.isFinite(p[2]);
915
+ return false;
916
+ }
917
+ /** The `[x, z]` of a point in either spelling. Assumes `isCoursePoint`. */
918
+ function coursePoint(p) {
919
+ return p.length === 3 ? [p[0] ?? 0, p[2] ?? 0] : [p[0] ?? 0, p[1] ?? 0];
920
+ }
921
+ /** A whole course, flattened to `[x, z]` whichever way it was written. */
922
+ function course(path) {
923
+ return path.map((p) => coursePoint(p));
924
+ }
925
+ /** What to tell an author who wrote neither spelling. */
926
+ const COURSE_SHAPE = "[x, z] — or [x, y, z], whose y is ignored";
927
+ //#endregion
855
928
  //#region src/3d/terrain/splat-material.ts
856
929
  /** Splice `code` immediately AFTER `marker` — a missing marker is a hard error
857
930
  * (a silently unpatched terrain shader renders weight-colored vertex soup). */
@@ -1716,7 +1789,7 @@ var Terrain3D = class extends Node3D {
1716
1789
  if (!Array.isArray(t.channels)) throw new IncantoError("BAD_FORMAT", `Terrain3D '${node.name}' channels must be an array of { path, width, depth, taper? }, got ${JSON.stringify(t.channels)}.`, { prop: "channels" });
1717
1790
  t.channels.forEach((c, i) => {
1718
1791
  const path = c.path;
1719
- if (!(c && typeof c === "object" && Array.isArray(path) && path.length >= 2 && path.every((p) => Array.isArray(p) && Number.isFinite(p[0]) && Number.isFinite(p[1])) && c.width > 0 && c.depth > 0 && (c.taper === void 0 || c.taper > 0))) throw new IncantoError("BAD_FORMAT", `Terrain3D '${node.name}' channels[${i}] needs a path of 2+ [x, z] points plus positive width and depth, got ${JSON.stringify(c)}.`, { prop: "channels" });
1792
+ if (!(c && typeof c === "object" && Array.isArray(path) && path.length >= 2 && path.every(isCoursePoint) && c.width > 0 && c.depth > 0 && (c.taper === void 0 || c.taper > 0))) throw new IncantoError("BAD_FORMAT", `Terrain3D '${node.name}' channels[${i}] needs a path of 2+ ${COURSE_SHAPE} points plus positive width and depth, got ${JSON.stringify(c)}.`, { prop: "channels" });
1720
1793
  });
1721
1794
  if (t.theme === "custom") {
1722
1795
  if (!Array.isArray(t.layers) || t.layers.length < 1 || t.layers.length > 4) throw new IncantoError("BAD_FORMAT", `Terrain3D '${node.name}' theme 'custom' needs 1–4 layers, got ${Array.isArray(t.layers) ? t.layers.length : typeof t.layers}.`, { prop: "layers" });
@@ -1759,10 +1832,17 @@ var Terrain3D = class extends Node3D {
1759
1832
  else this.externalChannels.set(owner, specs);
1760
1833
  this.externalRev++;
1761
1834
  }
1762
- /** Authored trenches plus the ones other nodes cut. */
1835
+ /**
1836
+ * Authored trenches plus the ones other nodes cut — every course flattened to
1837
+ * `[x, z]`, whichever spelling it was written in (see `course`).
1838
+ */
1763
1839
  allChannels() {
1764
- if (this.externalChannels.size === 0) return this.channels;
1765
- return [...this.channels, ...[...this.externalChannels.values()].flat()];
1840
+ const mine = this.channels.map((c) => ({
1841
+ ...c,
1842
+ path: course(c.path)
1843
+ }));
1844
+ if (this.externalChannels.size === 0) return mine;
1845
+ return [...mine, ...[...this.externalChannels.values()].flat()];
1766
1846
  }
1767
1847
  /** @internal The pure heightfield (lazily built — physics pulls this). */
1768
1848
  _heightmap() {
@@ -2613,6 +2693,18 @@ var BoneLookAt3D = class extends Node3D {
2613
2693
  lookedUpFor = "";
2614
2694
  /** 0..1 engage blend — eases in/out so tracking never snaps. */
2615
2695
  engage = 0;
2696
+ /**
2697
+ * How far the head is COMMITTED to the target, 0..1.
2698
+ *
2699
+ * `attachedBone` was a test hook for "did it find the bone"; this is the one
2700
+ * for "is it watching" — the whole observable effect of the node, and it eased
2701
+ * in and out where nothing could read it. A harness asking whether an NPC
2702
+ * tracks the player, and lets go when the player walks behind it, has no other
2703
+ * question to ask.
2704
+ */
2705
+ get engaged() {
2706
+ return this.engage;
2707
+ }
2616
2708
  /** Test hook. */
2617
2709
  get attachedBone() {
2618
2710
  return this.boneObj;
@@ -2679,7 +2771,7 @@ const parentScratch = new Quaternion();
2679
2771
  const quatScratch$1 = new Quaternion();
2680
2772
  //#endregion
2681
2773
  //#region src/3d/nodes/look-at.ts
2682
- const RAD2DEG$1 = 180 / Math.PI;
2774
+ const RAD2DEG$5 = 180 / Math.PI;
2683
2775
  /** World up. The whole fix is that this argument exists. */
2684
2776
  const UP = new Vector3(0, 1, 0);
2685
2777
  const eyeScratch = new Vector3();
@@ -2722,20 +2814,22 @@ function aimAt(node, warned, what) {
2722
2814
  lookScratch.lookAt(eyeScratch, targetScratch, UP);
2723
2815
  eulerScratch$1.setFromRotationMatrix(lookScratch, "XYZ");
2724
2816
  node.rotation = [
2725
- eulerScratch$1.x * RAD2DEG$1,
2726
- eulerScratch$1.y * RAD2DEG$1,
2727
- eulerScratch$1.z * RAD2DEG$1
2817
+ eulerScratch$1.x * RAD2DEG$5,
2818
+ eulerScratch$1.y * RAD2DEG$5,
2819
+ eulerScratch$1.z * RAD2DEG$5
2728
2820
  ];
2729
2821
  return warned;
2730
2822
  }
2731
2823
  //#endregion
2732
2824
  //#region src/3d/nodes/camera-3d.ts
2825
+ const DEG2RAD$1 = Math.PI / 180;
2826
+ const RAD2DEG$4 = 180 / Math.PI;
2733
2827
  /**
2734
2828
  * A perspective camera. Mark exactly one camera `current: true`; with none
2735
2829
  * marked, the renderer falls back to the first camera in tree order.
2736
2830
  * Aspect ratio is managed by the renderer on resize.
2737
2831
  */
2738
- var Camera3D = class extends Node3D {
2832
+ var Camera3D = class Camera3D extends Node3D {
2739
2833
  static typeName = "Camera3D";
2740
2834
  static props = {
2741
2835
  fov: {
@@ -2747,6 +2841,27 @@ var Camera3D = class extends Node3D {
2747
2841
  exclusiveMax: true
2748
2842
  }
2749
2843
  },
2844
+ /**
2845
+ * The least the player must see ACROSS, in degrees. `0` = off.
2846
+ *
2847
+ * `fov` is the VERTICAL field of view and the aspect comes from the canvas,
2848
+ * so a `fov: 60` camera shows 91° across a 16:9 window and **30° across a
2849
+ * 390×844 phone** — the same game, down a keyhole, held in portrait. Ask
2850
+ * for a width and the vertical fov widens until the screen carries it;
2851
+ * a window already wider than that keeps the fov it was authored with, so
2852
+ * this only ever ADDS view on a narrow screen.
2853
+ *
2854
+ * `65` is a good floor for a third-person game meant to be playable in one
2855
+ * hand. Off by default: no existing scene's framing moves.
2856
+ */
2857
+ minHorizontalFov: {
2858
+ default: 0,
2859
+ range: {
2860
+ min: 0,
2861
+ max: 180,
2862
+ exclusiveMax: true
2863
+ }
2864
+ },
2750
2865
  near: {
2751
2866
  default: .1,
2752
2867
  range: {
@@ -2765,6 +2880,17 @@ var Camera3D = class extends Node3D {
2765
2880
  lookAt: {
2766
2881
  default: "",
2767
2882
  nodePath: true
2883
+ },
2884
+ /**
2885
+ * Seconds the renderer FLIES from the last drawn pose to this camera when
2886
+ * it becomes current (0 = a cut). Every switch used to be a cut — a
2887
+ * cutscene's door camera, the return to play, a chase camera taking over.
2888
+ * The camera nodes are untouched: picking, framing and the editor read
2889
+ * the node; only the picture takes the long way round.
2890
+ */
2891
+ blendSeconds: {
2892
+ default: 0,
2893
+ range: { min: 0 }
2768
2894
  }
2769
2895
  };
2770
2896
  /** A frustum whose planes are the wrong way round draws nothing at all. */
@@ -2773,9 +2899,29 @@ var Camera3D = class extends Node3D {
2773
2899
  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.`);
2774
2900
  }
2775
2901
  fov = 60;
2902
+ minHorizontalFov = 0;
2776
2903
  near = .1;
2777
2904
  far = 1e3;
2778
2905
  current = false;
2906
+ /**
2907
+ * Make THIS the scene's camera and no other: the renderer takes the first
2908
+ * camera whose `current` is true in tree order, so setting your own without
2909
+ * clearing the gameplay camera's changed nothing visible. A handler a
2910
+ * connection can name — `triggerEnter → DoorCam.makeCurrent`.
2911
+ */
2912
+ makeCurrent(blendSeconds = void 0) {
2913
+ const root = this.tree?.root ?? this.getRoot();
2914
+ const walk = (n) => {
2915
+ if (n instanceof Camera3D) n.current = n === this;
2916
+ for (const c of n.children) walk(c);
2917
+ };
2918
+ walk(root);
2919
+ this.current = true;
2920
+ this._blendRequest = blendSeconds;
2921
+ }
2922
+ blendSeconds = 0;
2923
+ /** @internal `makeCurrent(seconds)` — read once by the renderer's sync. */
2924
+ _blendRequest = void 0;
2779
2925
  lookAt = "";
2780
2926
  /**
2781
2927
  * Aim, every frame, whenever `lookAt` names something — see `aimAt`, which
@@ -2789,11 +2935,26 @@ var Camera3D = class extends Node3D {
2789
2935
  _createObject3D() {
2790
2936
  return new PerspectiveCamera(this.fov, 1, this.near, this.far);
2791
2937
  }
2938
+ /**
2939
+ * The vertical fov to actually render with at this aspect.
2940
+ *
2941
+ * Plain `fov` unless `minHorizontalFov` asks for more width than the screen
2942
+ * carries, in which case it is widened until it does. Public because a game
2943
+ * that wants to know what it is really showing — a harness, a debug overlay —
2944
+ * could not otherwise ask, and the answer is not the authored number.
2945
+ */
2946
+ viewFov(aspect) {
2947
+ if (!(this.minHorizontalFov > 0) || !(aspect > 0)) return this.fov;
2948
+ const half = this.minHorizontalFov / 2 * DEG2RAD$1;
2949
+ const wanted = 2 * Math.atan(Math.tan(half) / aspect) * RAD2DEG$4;
2950
+ return Math.min(179.9, Math.max(this.fov, wanted));
2951
+ }
2792
2952
  _syncObject3D() {
2793
2953
  super._syncObject3D();
2794
2954
  const cam = this._ensureObject3D();
2795
- if (cam.fov !== this.fov || cam.near !== this.near || cam.far !== this.far) {
2796
- cam.fov = this.fov;
2955
+ const fov = this.viewFov(cam.aspect);
2956
+ if (cam.fov !== fov || cam.near !== this.near || cam.far !== this.far) {
2957
+ cam.fov = fov;
2797
2958
  cam.near = this.near;
2798
2959
  cam.far = this.far;
2799
2960
  cam.updateProjectionMatrix();
@@ -2811,7 +2972,12 @@ const MOVEMENT_STATES = [
2811
2972
  "walk",
2812
2973
  "run",
2813
2974
  "fastRun",
2814
- "airborne"
2975
+ "airborne",
2976
+ "swim",
2977
+ "climb",
2978
+ "crouch",
2979
+ "sneak",
2980
+ "dash"
2815
2981
  ];
2816
2982
  /** Air control stands down this long after a wall kick, so the kick lands. */
2817
2983
  const WALL_LOCKOUT_SECONDS = .18;
@@ -2820,13 +2986,27 @@ const DRAG_DAMPING = .15;
2820
2986
  const SPRING_K = 1.2;
2821
2987
  const SPRING_DAMPING = .08;
2822
2988
  const FALLING_MAX_VEL = -20;
2989
+ /** The swim spring: how hard the water returns the body to its waterline (1/s²) and damps it (1/s). */
2990
+ const SWIM_K = 30;
2991
+ const SWIM_DAMPING = 8;
2992
+ /** Lift cap while swimming, in g — water pushes back; it does not launch. */
2993
+ const SWIM_MAX_G = 3;
2994
+ const STANDARD_G = 9.81;
2995
+ /** A probe hit closer than this began inside the collider it hit. */
2996
+ const INSIDE_HIT = 1e-4;
2997
+ /** How fast a swimmer pulls themselves up onto a ledge (m/s). */
2998
+ const CLIMB_SPEED = 2;
2999
+ /** After letting go of a ladder, this long before it can be grabbed again (s). */
3000
+ const LADDER_LOCKOUT = .35;
3001
+ /** How often the controller re-scans the tree for Water3D nodes (fixed steps). */
3002
+ const WATER_SCAN_STEPS = 60;
2823
3003
  /** The schema's own default for an action prop — never a second copy of it. */
2824
3004
  function defaultActionOf(prop) {
2825
3005
  const schema = CharacterController3D.props[prop];
2826
3006
  return typeof schema?.default === "string" ? schema.default : "";
2827
3007
  }
2828
3008
  const LOOK_SPEED = .002;
2829
- const RAD2DEG = 180 / Math.PI;
3009
+ const RAD2DEG$3 = 180 / Math.PI;
2830
3010
  /** Spring-arm camera collision: keep the camera this far in front of an obstacle. */
2831
3011
  const CAM_COLLISION_MARGIN = .3;
2832
3012
  /** Never pull the orbit camera closer to the eye than this (avoids clipping into the skin). */
@@ -2853,8 +3033,21 @@ const UP_VEC = new Vector3(0, 1, 0);
2853
3033
  */
2854
3034
  var CharacterController3D = class extends Node3D {
2855
3035
  static typeName = "CharacterController3D";
2856
- static signals = ["movementStateChanged", "wallJumped"];
3036
+ static signals = [
3037
+ "movementStateChanged",
3038
+ "wallJumped",
3039
+ "lockChanged",
3040
+ "dashed"
3041
+ ];
2857
3042
  static props = {
3043
+ /**
3044
+ * Off = the controller lets go: no input, no forces, no camera, no
3045
+ * animation — the body is an ordinary RigidBody3D until it is on again.
3046
+ * The switch a game flips when the player gets into a car, sits in a
3047
+ * cutscene or opens a menu; flip the body's `enabled`/`visible` beside it
3048
+ * to park the character out of sight.
3049
+ */
3050
+ enabled: { default: true },
2858
3051
  view: {
2859
3052
  default: "free",
2860
3053
  options: [
@@ -2889,12 +3082,105 @@ var CharacterController3D = class extends Node3D {
2889
3082
  fallGravity: { default: 2.5 },
2890
3083
  floatHeight: { default: .01 },
2891
3084
  stepHeight: { default: .35 },
3085
+ /** Swimming pace (m/s) in water too deep to stand in; 0 = no swimming (the body sinks). */
3086
+ swimSpeed: { default: 1.6 },
3087
+ /** How far the body's origin rides BELOW the surface while swimming (m). */
3088
+ swimDepth: { default: .35 },
3089
+ /** Held: swim DOWN at `swimSpeed`. Absent from the input map = no diving. */
3090
+ diveAction: { default: "dive" },
3091
+ /** Ladder pace (m/s): push UP into an Area3D in `ladderGroup` and the body climbs it; 0 = no ladders. */
3092
+ climbSpeed: { default: 1.5 },
3093
+ /** Any Area3D in this group is a ladder. */
3094
+ ladderGroup: { default: "ladder" },
3095
+ /** On the way down from a jump, a ledge ahead no higher than this above the capsule's bottom is caught and pulled up onto (m); 0 = off. */
3096
+ mantleHeight: {
3097
+ default: 0,
3098
+ range: { min: 0 }
3099
+ },
3100
+ /**
3101
+ * The capsule's height while the crouch key is held (m); 0 = no crouch.
3102
+ * The body sinks by half the difference so the feet stay put, a duct
3103
+ * lower than the standing capsule is crawled through, and a ceiling that
3104
+ * will not let you stand keeps you down until you are out from under it.
3105
+ */
3106
+ crouchHeight: {
3107
+ default: 0,
3108
+ range: { min: 0 }
3109
+ },
3110
+ /** Pace while crouched, as a fraction of the standing pace. */
3111
+ crouchSpeedMultiplier: {
3112
+ default: .5,
3113
+ range: { min: 0 }
3114
+ },
3115
+ /** Held = crouch. Missing from the input map = the character never crouches. */
3116
+ crouchAction: { default: "crouch" },
3117
+ /**
3118
+ * A roll: this fast (m/s) for `dashSeconds`, where the stick points or —
3119
+ * with no stick — where the skin faces; 0 = no dash. The velocity is held
3120
+ * for the roll's frames (the controller brakes toward its own pace every
3121
+ * step, so a single impulse dies), `state` reads `dash`, and `dashed`
3122
+ * fires once per roll — hang the i-frames on it (`Health.protect`).
3123
+ */
3124
+ dashSpeed: {
3125
+ default: 0,
3126
+ range: { min: 0 }
3127
+ },
3128
+ dashSeconds: {
3129
+ default: .25,
3130
+ range: { min: 0 }
3131
+ },
3132
+ /** Seconds after a roll ends before the next can start. */
3133
+ dashCooldown: {
3134
+ default: 0,
3135
+ range: { min: 0 }
3136
+ },
3137
+ /** Pressed = roll. Missing from the input map = the character never dashes. */
3138
+ dashAction: { default: "dash" },
3139
+ /**
3140
+ * With no stick, roll BACKWARD — away from where the skin faces — instead
3141
+ * of forward: the Souls-like backstep a lock-on game wants (a neutral
3142
+ * roll into the thing you are locked on is the wrong way).
3143
+ */
3144
+ dashBackstep: { default: false },
2892
3145
  mouseLook: { default: true },
2893
3146
  zoomMin: { default: 0 },
2894
3147
  zoomMax: { default: 0 },
2895
3148
  pitchMin: { default: -1.3 },
2896
3149
  pitchMax: { default: 1.5 },
3150
+ /**
3151
+ * Where the camera RESTS before anyone has looked around, in degrees.
3152
+ * Negative looks down, which is what a third-person game usually wants.
3153
+ *
3154
+ * `pitch` is a radian field the mouse mutates, so the alternative was a
3155
+ * behaviour whose only job is one line in `onReady`. On a phone it stops
3156
+ * being a nicety: a portrait screen is very tall, so a level camera spends
3157
+ * the top half on sky and the bottom on the ground at the player's feet,
3158
+ * and a player with no mouse cannot fix it before they start dragging.
3159
+ * Clamped by `pitchMin`/`pitchMax` like every other pitch. `0` is the old
3160
+ * behaviour.
3161
+ */
3162
+ pitchStart: {
3163
+ default: 0,
3164
+ range: {
3165
+ min: -180,
3166
+ max: 180
3167
+ }
3168
+ },
2897
3169
  cameraCollision: { default: true },
3170
+ /**
3171
+ * Who owns the scene's camera: `'drive'` (default) is this controller, as
3172
+ * it always has been — the rig behind the character, per `view`. `'none'`
3173
+ * leaves the camera exactly where the scene puts it, for the whole shelf of
3174
+ * games with a FIXED view: a board game, a bomb arena, an isometric puzzle,
3175
+ * a fixed-angle horror. Those games could not use this controller at all,
3176
+ * because it re-posed their camera every frame; now they pair it with an
3177
+ * authored `Camera3D` (and a `FollowCamera` behaviour when the board
3178
+ * scrolls).
3179
+ */
3180
+ camera: {
3181
+ default: "drive",
3182
+ options: ["drive", "none"]
3183
+ },
2898
3184
  platformCarry: { default: true },
2899
3185
  turnSpeed: { default: 100 },
2900
3186
  camLerp: { default: 25 },
@@ -2906,6 +3192,20 @@ var CharacterController3D = class extends Node3D {
2906
3192
  nodePath: true
2907
3193
  },
2908
3194
  skinYawOffset: { default: 0 },
3195
+ /**
3196
+ * The button that LOCKS ON: the camera yaw follows the nearest live member
3197
+ * of `lockGroup` within `lockRange`, and the skin FACES it while the feet go
3198
+ * anywhere — a locked player circling a brute shows it a face, not a
3199
+ * shoulder, and a hitbox hung off the skin's yaw swings at it. Press again
3200
+ * to let go; the lock lets go by itself when the target dies, leaves the
3201
+ * tree, or gets 1.5 × `lockRange` away. '' = no lock-on.
3202
+ */
3203
+ lockOnAction: { default: "" },
3204
+ lockGroup: { default: "enemy" },
3205
+ lockRange: {
3206
+ default: 12,
3207
+ range: { min: 0 }
3208
+ },
2909
3209
  animations: {
2910
3210
  default: {},
2911
3211
  asset: {
@@ -2914,6 +3214,7 @@ var CharacterController3D = class extends Node3D {
2914
3214
  }
2915
3215
  }
2916
3216
  };
3217
+ enabled = true;
2917
3218
  view = "free";
2918
3219
  camDistance = 4;
2919
3220
  eyeHeight = .64;
@@ -2940,6 +3241,50 @@ var CharacterController3D = class extends Node3D {
2940
3241
  fallGravity = 2.5;
2941
3242
  floatHeight = .01;
2942
3243
  /**
3244
+ * Swimming pace in water too deep to stand in (m/s; `sprint` still
3245
+ * multiplies). `0` turns swimming OFF and the body sinks to the bed, which
3246
+ * is what a drowning pit wants and what every scene got before this.
3247
+ */
3248
+ swimSpeed = 1.6;
3249
+ /** Where the body's origin rides below the surface while swimming (m). */
3250
+ swimDepth = .35;
3251
+ /** Held: swim down at `swimSpeed`. Missing from the input map is fine. */
3252
+ diveAction = "dive";
3253
+ /**
3254
+ * Ladder pace (m/s). A ladder is any `Area3D` in `ladderGroup`: push UP into
3255
+ * it and the body climbs, gravity off, held to the rungs; at the top it
3256
+ * pulls itself onto the ledge, down steps off at the bottom, `jump` lets go.
3257
+ * `0` = no ladders (every scene before this).
3258
+ */
3259
+ climbSpeed = 1.5;
3260
+ ladderGroup = "ladder";
3261
+ /**
3262
+ * The pull-up, for any jump. A jump that falls a hand short of a ledge is
3263
+ * the most common near-miss in a platformer, and every one handles it the
3264
+ * same way: catch the edge, pull up. On the way down from a jump, a ledge
3265
+ * ahead no higher than this above the capsule's bottom is taken — the same
3266
+ * climb that takes a swimmer out of the water and a climber off a ladder.
3267
+ * `0` (the default) = a jump near a wall feels exactly as it did.
3268
+ */
3269
+ mantleHeight = 0;
3270
+ crouchHeight = 0;
3271
+ crouchSpeedMultiplier = .5;
3272
+ crouchAction = "crouch";
3273
+ dashSpeed = 0;
3274
+ dashSeconds = .25;
3275
+ dashCooldown = 0;
3276
+ dashAction = "dash";
3277
+ dashBackstep = false;
3278
+ dashLeft = 0;
3279
+ dashCooldownLeft = 0;
3280
+ dashDir = [0, -1];
3281
+ /** Read-only: shorter right now — by the key, or by a ceiling. */
3282
+ crouching = false;
3283
+ /** The capsule height to stand back up to. */
3284
+ standingHeight = 0;
3285
+ /** The skin's authored y offset, restored on standing. */
3286
+ skinRest = null;
3287
+ /**
2943
3288
  * How high a step the PLAYER walks up without jumping (0 = off).
2944
3289
  *
2945
3290
  * The hover spring holds the capsule a fixed distance above whatever is
@@ -2963,8 +3308,10 @@ var CharacterController3D = class extends Node3D {
2963
3308
  zoomMax = 0;
2964
3309
  pitchMin = -1.3;
2965
3310
  pitchMax = 1.5;
3311
+ pitchStart = 0;
2966
3312
  /** Spring-arm: pull the orbit camera in front of walls/ground instead of clipping. */
2967
3313
  cameraCollision = true;
3314
+ camera = "drive";
2968
3315
  /**
2969
3316
  * Ride whatever you are standing on.
2970
3317
  *
@@ -2989,19 +3336,59 @@ var CharacterController3D = class extends Node3D {
2989
3336
  * would fling the player on every step onto one.
2990
3337
  */
2991
3338
  groundCarry(ground, dt) {
3339
+ const body = this.parent;
3340
+ const physics = body?._physics3d;
3341
+ const me = body ? worldPosition(body) : [
3342
+ 0,
3343
+ 0,
3344
+ 0
3345
+ ];
2992
3346
  if (!this.platformCarry || !this.grounded || !ground || dt <= 0) {
2993
3347
  this.carriedBy = ground;
2994
- if (ground) this.carriedFrom = groundXZ(ground);
3348
+ if (ground) {
3349
+ this.carriedFrom = groundXZ(ground);
3350
+ this.carriedRot = physics?.rotationOf(ground) ?? [
3351
+ 0,
3352
+ 0,
3353
+ 0,
3354
+ 1
3355
+ ];
3356
+ this.carriedMe = [me[0] ?? 0, me[2] ?? 0];
3357
+ }
2995
3358
  return [0, 0];
2996
3359
  }
2997
3360
  const at = groundXZ(ground);
3361
+ const rot = physics?.rotationOf(ground) ?? [
3362
+ 0,
3363
+ 0,
3364
+ 0,
3365
+ 1
3366
+ ];
2998
3367
  const same = this.carriedBy === ground;
2999
3368
  const was = this.carriedFrom;
3369
+ const wasRot = this.carriedRot;
3370
+ const wasMe = this.carriedMe;
3000
3371
  this.carriedBy = ground;
3001
3372
  this.carriedFrom = at;
3373
+ this.carriedRot = rot;
3374
+ this.carriedMe = [me[0] ?? 0, me[2] ?? 0];
3002
3375
  if (!same) return [0, 0];
3003
- return [(at[0] - was[0]) / dt, (at[1] - was[1]) / dt];
3004
- }
3376
+ let vx = (at[0] - was[0]) / dt;
3377
+ let vz = (at[1] - was[1]) / dt;
3378
+ const ox = wasMe[0] - was[0];
3379
+ const oz = wasMe[1] - was[1];
3380
+ const [rx, , rz] = rotateXZByDelta(rot, wasRot, ox, oz);
3381
+ vx += (rx - ox) / dt;
3382
+ vz += (rz - oz) / dt;
3383
+ return [vx, vz];
3384
+ }
3385
+ carriedRot = [
3386
+ 0,
3387
+ 0,
3388
+ 0,
3389
+ 1
3390
+ ];
3391
+ carriedMe = [0, 0];
3005
3392
  turnSpeed = 100;
3006
3393
  /** Camera smoothing rate (1-exp(-rate·dt)); side view uses 5. */
3007
3394
  camLerp = 25;
@@ -3016,6 +3403,68 @@ var CharacterController3D = class extends Node3D {
3016
3403
  * character facing away from the camera until it first moves).
3017
3404
  */
3018
3405
  skinYawOffset = 0;
3406
+ lockOnAction = "";
3407
+ lockGroup = "enemy";
3408
+ lockRange = 12;
3409
+ /** What the character is locked onto, or null. */
3410
+ lockTarget = null;
3411
+ /** Lock onto `node`: the camera yaw and the skin follow it from now on. */
3412
+ lock(node) {
3413
+ if (node === this.lockTarget) return;
3414
+ this.lockTarget = node;
3415
+ this.emit("lockChanged", node);
3416
+ }
3417
+ /** Let go. */
3418
+ unlock() {
3419
+ if (!this.lockTarget) return;
3420
+ this.lockTarget = null;
3421
+ this.emit("lockChanged", null);
3422
+ }
3423
+ /** Lock the nearest live, visible member of `lockGroup` within `lockRange` — or let go if locked. */
3424
+ toggleLock() {
3425
+ if (this.lockTarget) {
3426
+ this.unlock();
3427
+ return;
3428
+ }
3429
+ const body = this.parent;
3430
+ if (!body || !hasPositionArray(body)) return;
3431
+ const me = worldPosition(body);
3432
+ let best = null;
3433
+ let bestD = this.lockRange;
3434
+ for (const n of this.tree?.getNodesInGroup(this.lockGroup) ?? []) {
3435
+ if (n === body || !hasPositionArray(n)) continue;
3436
+ if (n.visible === false) continue;
3437
+ if (findHealth(n)?.health.isDead) continue;
3438
+ const p = worldPosition(n);
3439
+ const d = Math.hypot((p[0] ?? 0) - (me[0] ?? 0), (p[2] ?? 0) - (me[2] ?? 0));
3440
+ if (d < bestD) {
3441
+ bestD = d;
3442
+ best = n;
3443
+ }
3444
+ }
3445
+ if (best) this.lock(best);
3446
+ }
3447
+ /** Player → target on the ground plane, unit; null when not locked (and lets go when the lock is stale). */
3448
+ lockDirection() {
3449
+ const t = this.lockTarget;
3450
+ if (!t) return null;
3451
+ const body = this.parent;
3452
+ if (!t.tree || !body || !hasPositionArray(body) || !hasPositionArray(t) || findHealth(t)?.health.isDead) {
3453
+ this.unlock();
3454
+ return null;
3455
+ }
3456
+ const me = worldPosition(body);
3457
+ const p = worldPosition(t);
3458
+ const dx = (p[0] ?? 0) - (me[0] ?? 0);
3459
+ const dz = (p[2] ?? 0) - (me[2] ?? 0);
3460
+ const d = Math.hypot(dx, dz);
3461
+ if (d > this.lockRange * 1.5) {
3462
+ this.unlock();
3463
+ return null;
3464
+ }
3465
+ if (d < .05) return null;
3466
+ return [dx / d, dz / d];
3467
+ }
3019
3468
  /**
3020
3469
  * Movement-state → animation asset map, applied to the `skinPath` model
3021
3470
  * directly — no CharacterAnimator behavior needed:
@@ -3047,9 +3496,29 @@ var CharacterController3D = class extends Node3D {
3047
3496
  /** Camera yaw/pitch in radians (mouse look mutates these). */
3048
3497
  yaw = 0;
3049
3498
  pitch = 0;
3050
- /** idle | walk | run | fastRun | airborne — drive animations from this. */
3499
+ /** idle | walk | run | fastRun | airborne | swim — drive animations from this. */
3051
3500
  state = "idle";
3052
3501
  grounded = false;
3502
+ /** In water too deep to stand in — held at the surface by the swim spring. */
3503
+ swimming = false;
3504
+ /** The head is under the surface (diving, or a wave over it). */
3505
+ submerged = false;
3506
+ /** Pulling up onto a ledge out of the water (see the climb in fixedUpdate). */
3507
+ climbing = false;
3508
+ /** On a ladder — an `Area3D` in `ladderGroup` the body overlaps and pushed up into. */
3509
+ onLadder = false;
3510
+ ladders = [];
3511
+ ladderScanIn = 0;
3512
+ ladderLockout = 0;
3513
+ /** Every Water3D in the tree, re-scanned every {@link WATER_SCAN_STEPS}. */
3514
+ waters = [];
3515
+ waterScanIn = 0;
3516
+ /**
3517
+ * A hop out of the water is in progress: the spring may catch a ledge on
3518
+ * the way down (the mantle) — a jump from land never gets that, or the
3519
+ * spring would cushion every landing.
3520
+ */
3521
+ swimHop = false;
3053
3522
  /** @internal Feel timers — see the jump block for what each one buys. */
3054
3523
  coyoteLeft = 0;
3055
3524
  bufferLeft = 0;
@@ -3068,6 +3537,10 @@ var CharacterController3D = class extends Node3D {
3068
3537
  }
3069
3538
  onEnterTree() {
3070
3539
  this.body;
3540
+ if (this.pitchStart !== 0) {
3541
+ const want = this.pitchStart * Math.PI / 180;
3542
+ this.pitch = Math.min(Math.max(want, -this.pitchMax), -this.pitchMin);
3543
+ }
3071
3544
  if (this.skinPath !== "") {
3072
3545
  const skin = this.getNodeOrNull(this.skinPath);
3073
3546
  if (skin) skin.rotation = [
@@ -3082,9 +3555,15 @@ var CharacterController3D = class extends Node3D {
3082
3555
  const physics = body._physics3d;
3083
3556
  const input = this.tree?.engine?.input;
3084
3557
  if (!physics || !input) return;
3558
+ if (!this.enabled) {
3559
+ this.readAction("moveAction", () => input.getVector(this.moveAction), null, "");
3560
+ this.readAction("jumpAction", () => input.isPressed(this.jumpAction), false, "");
3561
+ this.readAction("sprintAction", () => input.isPressed(this.sprintAction), false, "");
3562
+ return;
3563
+ }
3085
3564
  const radius = body.collider.radius ?? .32;
3086
3565
  const halfHeight = (body.collider.height ?? 1) / 2;
3087
- const pos = worldOf(body);
3566
+ const pos = worldOf$1(body);
3088
3567
  const vel = physics.velocityOf(body);
3089
3568
  const mass = physics.massOf(body);
3090
3569
  const floatingDis = radius + this.floatHeight;
@@ -3109,7 +3588,7 @@ var CharacterController3D = class extends Node3D {
3109
3588
  -1,
3110
3589
  0
3111
3590
  ], rayLen, body);
3112
- if (hit) {
3591
+ if (hit && hit.distance > INSIDE_HIT) {
3113
3592
  const d = hit.distance;
3114
3593
  if (toi === null || d < toi) {
3115
3594
  toi = d;
@@ -3121,6 +3600,26 @@ var CharacterController3D = class extends Node3D {
3121
3600
  this.grounded = toi !== null && toi < floatingDis * 2;
3122
3601
  const carry = this.groundCarry(ground, dt);
3123
3602
  const footed = toi !== null && toi < floatingDis * 1.3 && (vel[1] ?? 0) <= .5;
3603
+ const water = this.swimSpeed > 0 ? this.surfaceAt(pos[0], pos[2]) : null;
3604
+ const surface = water === null ? null : water[0];
3605
+ const waterline = surface === null ? null : surface - this.swimDepth;
3606
+ const standY = toi === null ? null : originY - toi + floatingDis + halfHeight;
3607
+ const swimming = waterline !== null && surface !== null && surface > pos[1] - halfHeight && waterline !== null && (standY === null || waterline > standY) && !(this.jumping && (vel[1] ?? 0) > .5);
3608
+ this.swimming = swimming;
3609
+ this.submerged = surface !== null && surface > pos[1] + halfHeight;
3610
+ if (swimming) {
3611
+ this.jumping = false;
3612
+ this.rising = false;
3613
+ this.jumpsUsed = 0;
3614
+ this.coyoteLeft = 0;
3615
+ this.swimHop = false;
3616
+ }
3617
+ const mantling = this.swimHop && (vel[1] ?? 0) <= .5;
3618
+ if (this.lockOnAction !== "") {
3619
+ if (this.readAction("lockOnAction", () => input.justPressed(this.lockOnAction), false, "Lock-on will not toggle.")) this.toggleLock();
3620
+ }
3621
+ const lockDir = this.lockDirection();
3622
+ if (lockDir) this.yaw = Math.atan2(-lockDir[0], -lockDir[1]);
3124
3623
  let mx = 0;
3125
3624
  let mz = 0;
3126
3625
  let moving = false;
@@ -3139,12 +3638,57 @@ var CharacterController3D = class extends Node3D {
3139
3638
  } catch (error) {
3140
3639
  this.reportMissingAction("moveAction", error, "The character will not move.");
3141
3640
  }
3641
+ const hitHealth = findHealth(this.parent)?.health ?? null;
3642
+ const staggered = hitHealth?.staggered ?? false;
3643
+ if (staggered) {
3644
+ mx = 0;
3645
+ mz = 0;
3646
+ moving = false;
3647
+ }
3648
+ const kick = hitHealth?.knocked ? hitHealth.kickVelocity : [];
3649
+ const lift = hitHealth?.knocked ? hitHealth.takeLift() : 0;
3650
+ this.dashCooldownLeft = Math.max(0, this.dashCooldownLeft - dt);
3651
+ if (!staggered && this.dashSpeed > 0 && this.readAction("dashAction", () => input.justPressed(this.dashAction), false, "The character will never dash.") && this.dashLeft <= 0 && this.dashCooldownLeft <= 0 && !this.onLadder && !this.crouching) {
3652
+ this.dashLeft = this.dashSeconds;
3653
+ this.dashCooldownLeft = this.dashSeconds + this.dashCooldown;
3654
+ if (moving) this.dashDir = [mx, mz];
3655
+ else {
3656
+ const skin = this.skinPath !== "" ? this.getNodeOrNull(this.skinPath) : null;
3657
+ if (skin) {
3658
+ const yaw = ((skin.rotation[1] ?? 0) - this.skinYawOffset) * (Math.PI / 180);
3659
+ const back = this.dashBackstep ? -1 : 1;
3660
+ this.dashDir = [Math.sin(yaw) * back, Math.cos(yaw) * back];
3661
+ }
3662
+ }
3663
+ this.emit("dashed");
3664
+ }
3665
+ const dashing = this.dashLeft > 0;
3666
+ if (dashing) this.dashLeft -= dt;
3667
+ const rawUp = -this.readAction("moveAction", () => input.getVector(this.moveAction).y, 0, "The character will not climb.");
3142
3668
  const sprinting = this.readAction("sprintAction", () => input.isPressed(this.sprintAction), false, "Sprint will never engage.");
3143
3669
  const intensity = keyboardIntensity(moving, sprinting);
3144
3670
  const speedMultiplier = 1 + (this.sprintMultiplier - 1) * intensity;
3145
- const targetSpeed = moving ? this.maxSpeed * speedMultiplier : 0;
3671
+ const walkMultiplier = 1 + (this.sprintMultiplier - 1) * keyboardIntensity(true, false);
3672
+ const targetSpeed = !moving ? 0 : swimming ? this.swimSpeed * speedMultiplier / walkMultiplier : this.maxSpeed * speedMultiplier * (this.crouching ? this.crouchSpeedMultiplier : 1);
3673
+ this.ladderLockout = Math.max(0, this.ladderLockout - dt);
3674
+ const ladder = this.climbSpeed > 0 && !swimming && this.ladderLockout <= 0 ? this.ladderUnder(body) : null;
3675
+ let onLadder = false;
3676
+ if (ladder) {
3677
+ const wantsUp = rawUp > .1;
3678
+ const wantsDown = rawUp < -.1;
3679
+ if (this.onLadder) onLadder = !(footed && wantsDown);
3680
+ else if (wantsUp || wantsDown && !footed) onLadder = true;
3681
+ }
3682
+ this.onLadder = onLadder;
3683
+ if (onLadder) {
3684
+ this.jumping = false;
3685
+ this.rising = false;
3686
+ this.jumpsUsed = 0;
3687
+ this.coyoteLeft = 0;
3688
+ this.swimHop = false;
3689
+ }
3146
3690
  this.wallNormal = null;
3147
- if ((this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0)) && !this.grounded && moving) {
3691
+ if ((this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0)) && !this.grounded && !swimming && !onLadder && moving) {
3148
3692
  const r = body.collider.radius ?? .32;
3149
3693
  const n = (this.tree?.engine?.physics?.castRay([
3150
3694
  pos[0] ?? 0,
@@ -3165,7 +3709,42 @@ var CharacterController3D = class extends Node3D {
3165
3709
  }
3166
3710
  }
3167
3711
  this.wallLockout = Math.max(0, this.wallLockout - dt);
3168
- const control = this.wallLockout > 0 ? 0 : this.grounded ? 1 : this.airControl;
3712
+ let climbing = false;
3713
+ const mantleReach = this.mantleHeight > 0 && this.jumping && (vel[1] ?? 0) <= .5;
3714
+ const reach = mantleReach ? Math.max(this.mantleHeight, this.stepHeight) : this.stepHeight;
3715
+ if (reach > 0 && moving && (swimming || mantling || onLadder || mantleReach || this.climbing)) {
3716
+ const ahead = radius + .12;
3717
+ const stepFrom = water === null ? originY : water[1];
3718
+ const probeTop = stepFrom + reach + .2;
3719
+ const ledge = physics.castRay([
3720
+ pos[0] + mx * ahead,
3721
+ probeTop,
3722
+ pos[2] + mz * ahead
3723
+ ], [
3724
+ 0,
3725
+ -1,
3726
+ 0
3727
+ ], reach + .2 + rayLen, body);
3728
+ if (ledge && ledge.distance > INSIDE_HIT) {
3729
+ const top = probeTop - ledge.distance;
3730
+ const aboveTheBottom = top > originY - radius + .02;
3731
+ const withinReach = top - stepFrom <= reach + .02;
3732
+ const stillBelow = originY < top + floatingDis - .01;
3733
+ if (aboveTheBottom && withinReach && stillBelow) climbing = true;
3734
+ }
3735
+ }
3736
+ this.climbing = climbing;
3737
+ if (climbing && (vel[1] ?? 0) < CLIMB_SPEED) body.applyImpulse([
3738
+ 0,
3739
+ (CLIMB_SPEED - (vel[1] ?? 0)) * .5 * mass,
3740
+ 0
3741
+ ]);
3742
+ if (onLadder && !climbing) body.applyImpulse([
3743
+ -(vel[0] ?? 0) * .3 * mass,
3744
+ (rawUp * this.climbSpeed - (vel[1] ?? 0)) * .5 * mass,
3745
+ -(vel[2] ?? 0) * .3 * mass
3746
+ ]);
3747
+ const control = this.wallLockout > 0 || onLadder && !climbing ? 0 : this.grounded || swimming || climbing ? 1 : this.airControl;
3169
3748
  const ax = (mx * targetSpeed + carry[0] - vel[0]) / ACC_DELTA_TIME * control;
3170
3749
  const az = (mz * targetSpeed + carry[1] - vel[2]) / ACC_DELTA_TIME * control;
3171
3750
  body.applyImpulse([
@@ -3173,13 +3752,13 @@ var CharacterController3D = class extends Node3D {
3173
3752
  0,
3174
3753
  az * mass
3175
3754
  ]);
3176
- if (!moving && this.grounded) body.applyImpulse([
3755
+ if (!moving && (this.grounded || swimming)) body.applyImpulse([
3177
3756
  -(vel[0] - carry[0]) * DRAG_DAMPING * mass,
3178
3757
  0,
3179
3758
  -(vel[2] - carry[1]) * DRAG_DAMPING * mass
3180
3759
  ]);
3181
3760
  let springToi = toi;
3182
- if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && (vel[1] ?? 0) <= .5) {
3761
+ if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && !swimming && !climbing && (vel[1] ?? 0) <= .5) {
3183
3762
  const ahead = radius + .12;
3184
3763
  const probeTop = originY + this.stepHeight + .05;
3185
3764
  const stepHit = physics.castRay([
@@ -3191,12 +3770,12 @@ var CharacterController3D = class extends Node3D {
3191
3770
  -1,
3192
3771
  0
3193
3772
  ], this.stepHeight + .05 + rayLen, body);
3194
- if (stepHit) {
3773
+ if (stepHit && stepHit.distance > INSIDE_HIT) {
3195
3774
  const rise = probeTop - stepHit.distance - (originY - toi);
3196
3775
  if (rise > .02 && rise <= this.stepHeight + .02) springToi = toi - rise;
3197
3776
  }
3198
3777
  }
3199
- if (springToi !== null && springToi < floatingDis * 2 && !this.jumping && (vel[1] ?? 0) < 2) {
3778
+ if (springToi !== null && springToi < floatingDis * 2 && (!this.jumping || mantling) && !swimming && !climbing && !onLadder && (vel[1] ?? 0) < 2) {
3200
3779
  const spring = SPRING_K * (floatingDis - springToi) - vel[1] * SPRING_DAMPING;
3201
3780
  body.applyImpulse([
3202
3781
  0,
@@ -3204,21 +3783,94 @@ var CharacterController3D = class extends Node3D {
3204
3783
  0
3205
3784
  ]);
3206
3785
  }
3786
+ const diving = this.swimSpeed > 0 && this.readAction("diveAction", () => input.isPressed(this.diveAction), false, "The character will never dive.");
3787
+ if (swimming && !climbing && waterline !== null) {
3788
+ const surfacing = this.submerged && this.readAction("jumpAction", () => input.isPressed(this.jumpAction), false, "The character will never surface by choice.");
3789
+ const vy = vel[1] ?? 0;
3790
+ let ay;
3791
+ if (diving) ay = (-this.swimSpeed - vy) / ACC_DELTA_TIME / dt;
3792
+ else if (surfacing) ay = (this.swimSpeed - vy) / ACC_DELTA_TIME / dt;
3793
+ else ay = SWIM_K * (waterline - pos[1]) - SWIM_DAMPING * vy;
3794
+ ay = Math.max(-3 * STANDARD_G, Math.min(SWIM_MAX_G * STANDARD_G, ay));
3795
+ body.applyImpulse([
3796
+ 0,
3797
+ ay * dt * mass,
3798
+ 0
3799
+ ]);
3800
+ }
3207
3801
  if (footed) {
3208
3802
  this.coyoteLeft = this.coyoteSeconds;
3209
3803
  this.jumpsUsed = 0;
3210
3804
  this.rising = false;
3211
3805
  this.jumping = false;
3806
+ this.swimHop = false;
3212
3807
  } else {
3213
3808
  this.coyoteLeft = Math.max(0, this.coyoteLeft - dt);
3214
- if (!this.grounded && this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
3809
+ if (!this.grounded && !swimming && this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
3215
3810
  }
3216
3811
  const pressedJump = this.readAction("jumpAction", () => input.justPressed(this.jumpAction), false, "The character will never jump.");
3217
3812
  if (pressedJump) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
3218
3813
  else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
3219
- const wantsJump = this.bufferLeft > 0 || pressedJump;
3814
+ let wantsJump = this.bufferLeft > 0 || pressedJump;
3815
+ if (staggered) wantsJump = false;
3816
+ const wantCrouch = this.crouchHeight > 0 && this.readAction("crouchAction", () => input.isPressed(this.crouchAction), false, "The character will never crouch.");
3817
+ if (!this.crouching) {
3818
+ const standing = halfHeight * 2;
3819
+ if (wantCrouch && this.grounded && this.crouchHeight < standing) {
3820
+ this.standingHeight = standing;
3821
+ this.crouching = true;
3822
+ this.setHeight(body, this.crouchHeight, standing);
3823
+ }
3824
+ } else {
3825
+ if (!wantCrouch) {
3826
+ const rise = this.standingHeight - this.crouchHeight;
3827
+ const top = pos[1] + this.crouchHeight / 2 + radius - .02;
3828
+ if (!physics.castRay([
3829
+ pos[0],
3830
+ top,
3831
+ pos[2]
3832
+ ], [
3833
+ 0,
3834
+ 1,
3835
+ 0
3836
+ ], rise + .06, body)) {
3837
+ this.crouching = false;
3838
+ this.setHeight(body, this.standingHeight, this.crouchHeight);
3839
+ }
3840
+ }
3841
+ if (this.crouching) {
3842
+ wantsJump = false;
3843
+ this.bufferLeft = 0;
3844
+ }
3845
+ }
3220
3846
  const wall = this.wallNormal;
3221
- if (wantsJump && !this.grounded && wall && this.wallJumpImpulse.some((v) => v !== 0)) {
3847
+ if (wantsJump && onLadder && ladder) {
3848
+ const at = worldOf$1(ladder);
3849
+ let ax = pos[0] - at[0];
3850
+ let az = pos[2] - at[2];
3851
+ const len = Math.hypot(ax, az);
3852
+ if (len > .001) {
3853
+ ax /= len;
3854
+ az /= len;
3855
+ } else {
3856
+ ax = -mx;
3857
+ az = -mz;
3858
+ }
3859
+ body.linearVelocity = [
3860
+ ax * 2,
3861
+ 2.5,
3862
+ az * 2
3863
+ ];
3864
+ vel[0] = ax * 2;
3865
+ vel[1] = 2.5;
3866
+ vel[2] = az * 2;
3867
+ this.onLadder = false;
3868
+ this.ladderLockout = LADDER_LOCKOUT;
3869
+ this.bufferLeft = 0;
3870
+ this.rising = false;
3871
+ this.jumping = true;
3872
+ this.jumpsUsed = this.maxJumps;
3873
+ } else if (wantsJump && !this.grounded && wall && this.wallJumpImpulse.some((v) => v !== 0)) {
3222
3874
  const away = this.wallJumpImpulse[0] ?? 0;
3223
3875
  const up = this.wallJumpImpulse[1] ?? 0;
3224
3876
  body.linearVelocity = [
@@ -3236,8 +3888,10 @@ var CharacterController3D = class extends Node3D {
3236
3888
  this.jumping = true;
3237
3889
  this.wallLockout = WALL_LOCKOUT_SECONDS;
3238
3890
  this.emit("wallJumped", wall);
3239
- } else if (wantsJump && (footed || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
3240
- const jumpVel = this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
3891
+ } else if (wantsJump && !onLadder && !(swimming && this.submerged) && (footed || swimming || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
3892
+ const hopVel = swimming ? Math.max(this.jumpVelocity, Math.sqrt(2 * this.sceneGravity() * (this.swimDepth + halfHeight + this.stepHeight + .05))) : 0;
3893
+ const jumpVel = swimming ? hopVel : this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
3894
+ if (swimming) this.swimHop = true;
3241
3895
  body.linearVelocity = [
3242
3896
  vel[0],
3243
3897
  jumpVel,
@@ -3265,6 +3919,7 @@ var CharacterController3D = class extends Node3D {
3265
3919
  }
3266
3920
  }
3267
3921
  const sliding = this.wallSlideSpeed > 0 && !footed && this.wallNormal !== null && (vel[1] ?? 0) < 0;
3922
+ const atSlideCap = sliding && (vel[1] ?? 0) <= -this.wallSlideSpeed + .01;
3268
3923
  if (sliding && (vel[1] ?? 0) < -this.wallSlideSpeed) {
3269
3924
  body.linearVelocity = [
3270
3925
  vel[0],
@@ -3273,7 +3928,8 @@ var CharacterController3D = class extends Node3D {
3273
3928
  ];
3274
3929
  vel[1] = -this.wallSlideSpeed;
3275
3930
  }
3276
- if ((vel[1] ?? 0) < FALLING_MAX_VEL) body.gravityScale = 0;
3931
+ if (swimming || climbing || atSlideCap || this.onLadder) body.gravityScale = 0;
3932
+ else if ((vel[1] ?? 0) < FALLING_MAX_VEL) body.gravityScale = 0;
3277
3933
  else if ((vel[1] ?? 0) < 0 && !footed) body.gravityScale = sliding ? 1 : this.fallGravity;
3278
3934
  else body.gravityScale = 1;
3279
3935
  if (this.view === "side" && Math.abs(pos[2]) > .01) body.position = [
@@ -3281,7 +3937,21 @@ var CharacterController3D = class extends Node3D {
3281
3937
  body.position[1] ?? 0,
3282
3938
  0
3283
3939
  ];
3284
- const next = movementState(!(this.jumping || !this.grounded), moving ? intensity : 0);
3940
+ if (dashing && !swimming) body.linearVelocity = [
3941
+ this.dashDir[0] * this.dashSpeed,
3942
+ vel[1] ?? 0,
3943
+ this.dashDir[1] * this.dashSpeed
3944
+ ];
3945
+ if (kick.length > 0) {
3946
+ const cur = body.linearVelocity;
3947
+ body.linearVelocity = [
3948
+ kick[0] ?? 0,
3949
+ lift !== 0 ? lift : cur[1] ?? 0,
3950
+ kick[2] ?? 0
3951
+ ];
3952
+ }
3953
+ const airborne = this.jumping || !this.grounded;
3954
+ const next = dashing && !swimming ? "dash" : swimming ? "swim" : this.onLadder || climbing ? "climb" : this.crouching && !airborne ? moving ? "sneak" : "crouch" : movementState(!airborne, moving ? intensity : 0);
3285
3955
  if (next !== this.state) {
3286
3956
  this.state = next;
3287
3957
  const clip = this.animations[next];
@@ -3291,13 +3961,15 @@ var CharacterController3D = class extends Node3D {
3291
3961
  }
3292
3962
  this.emit("movementStateChanged", next);
3293
3963
  }
3294
- if (moving && this.skinPath !== "") {
3964
+ if ((moving || lockDir) && this.skinPath !== "") {
3295
3965
  const skin = this.getNodeOrNull(this.skinPath);
3296
3966
  if (skin) {
3297
- const target = Math.atan2(mx, mz) * RAD2DEG + this.skinYawOffset;
3967
+ const fx = lockDir ? lockDir[0] : mx;
3968
+ const fz = lockDir ? lockDir[1] : mz;
3969
+ const target = Math.atan2(fx, fz) * RAD2DEG$3 + this.skinYawOffset;
3298
3970
  const current = skin.rotation[1] ?? 0;
3299
3971
  let delta = (target - current + 540) % 360 - 180;
3300
- const step = this.turnSpeed * dt * RAD2DEG;
3972
+ const step = this.turnSpeed * dt * RAD2DEG$3;
3301
3973
  if (Math.abs(delta) > step) delta = Math.sign(delta) * step;
3302
3974
  skin.rotation = [
3303
3975
  skin.rotation[0] ?? 0,
@@ -3310,6 +3982,76 @@ var CharacterController3D = class extends Node3D {
3310
3982
  /** Actions already reported missing, so a per-frame read says it once. */
3311
3983
  reportedActions = /* @__PURE__ */ new Set();
3312
3984
  /**
3985
+ * The water over a world point — `[wave surface, still level]` — or null
3986
+ * when no `Water3D`'s footprint covers it. The tree is re-scanned once a
3987
+ * second, so a lake streamed in later is found; the wave height is the CPU
3988
+ * twin of what is drawn, so a swimmer rides the swell the way a raft does.
3989
+ */
3990
+ /** The ladder the body is overlapping, if any — an Area3D in `ladderGroup`. */
3991
+ ladderUnder(body) {
3992
+ const physics = body._physics3d;
3993
+ if (!physics) return null;
3994
+ if (this.ladderScanIn <= 0) {
3995
+ this.ladders.length = 0;
3996
+ collectAreas(this.getRoot(), this.ladderGroup, this.ladders);
3997
+ this.ladderScanIn = WATER_SCAN_STEPS;
3998
+ }
3999
+ this.ladderScanIn -= 1;
4000
+ for (const ladder of this.ladders) {
4001
+ if (ladder.tree === null) continue;
4002
+ if (physics.overlapping(ladder).includes(body)) return ladder;
4003
+ }
4004
+ return null;
4005
+ }
4006
+ /**
4007
+ * Swap the capsule's height and keep the feet where they are: the body
4008
+ * drops (or rises) by half the difference in the same step, the skin's
4009
+ * authored offset moves the other way so the model's feet stay on the
4010
+ * floor, and the adapter rebuilds the body at the new place next step.
4011
+ */
4012
+ setHeight(body, next, from) {
4013
+ body.collider = {
4014
+ ...body.collider,
4015
+ height: next
4016
+ };
4017
+ const d = (from - next) / 2;
4018
+ const p = body.position;
4019
+ body.position = [
4020
+ p[0] ?? 0,
4021
+ (p[1] ?? 0) - d,
4022
+ p[2] ?? 0
4023
+ ];
4024
+ if (this.skinPath === "") return;
4025
+ const skin = this.getNodeOrNull(this.skinPath);
4026
+ if (!skin) return;
4027
+ if (this.skinRest === null) this.skinRest = skin.position[1] ?? 0;
4028
+ const lift = this.crouching ? (this.standingHeight - this.crouchHeight) / 2 : 0;
4029
+ skin.position = [
4030
+ skin.position[0] ?? 0,
4031
+ this.skinRest + lift,
4032
+ skin.position[2] ?? 0
4033
+ ];
4034
+ }
4035
+ sceneGravity() {
4036
+ const g = this.tree?.engine?.scene?.physics?.gravity;
4037
+ return Math.abs(g?.[1] ?? STANDARD_G) || STANDARD_G;
4038
+ }
4039
+ surfaceAt(x, z) {
4040
+ if (this.waterScanIn <= 0) {
4041
+ this.waters.length = 0;
4042
+ collectWaters(this.getRoot(), this.waters);
4043
+ this.waterScanIn = WATER_SCAN_STEPS;
4044
+ }
4045
+ this.waterScanIn -= 1;
4046
+ for (const water of this.waters) {
4047
+ if (water.tree === null) continue;
4048
+ const still = water._waterYAt(x, z);
4049
+ if (still === null) continue;
4050
+ return [water.heightAt(x, z), still];
4051
+ }
4052
+ return null;
4053
+ }
4054
+ /**
3313
4055
  * Read an input action this controller is allowed to be missing.
3314
4056
  *
3315
4057
  * Tolerated because a game may declare no sprint key and no jump; reported
@@ -3333,6 +4075,7 @@ var CharacterController3D = class extends Node3D {
3333
4075
  }, void 0, consequence);
3334
4076
  }
3335
4077
  update(dt) {
4078
+ if (!this.enabled) return;
3336
4079
  const input = this.tree?.engine?.input;
3337
4080
  if (!input) return;
3338
4081
  if (this.mouseLook) {
@@ -3346,11 +4089,12 @@ var CharacterController3D = class extends Node3D {
3346
4089
  const wheel = input.wheelDelta();
3347
4090
  if (wheel !== 0 && this.zoomMax > this.zoomMin) this.camDistance = clamp(this.camDistance + wheel * LOOK_SPEED, this.zoomMin, this.zoomMax);
3348
4091
  }
4092
+ if (this.camera === "none") return;
3349
4093
  const camera = this.findCamera();
3350
4094
  if (!camera) return;
3351
4095
  const body = this.parent instanceof RigidBody3D ? this.parent : null;
3352
4096
  if (!body) return;
3353
- const playerPos = worldOf(body);
4097
+ const playerPos = worldOf$1(body);
3354
4098
  const pivotRate = this.view === "free" ? 11 : 1e3;
3355
4099
  const pk = 1 - Math.exp(-pivotRate * dt);
3356
4100
  const pivot = this.pivot ?? [
@@ -3404,9 +4148,9 @@ var CharacterController3D = class extends Node3D {
3404
4148
  eulerScratch.setFromQuaternion(quatScratch, "XYZ");
3405
4149
  }
3406
4150
  camera.rotation = [
3407
- eulerScratch.x * RAD2DEG,
3408
- eulerScratch.y * RAD2DEG,
3409
- eulerScratch.z * RAD2DEG
4151
+ eulerScratch.x * RAD2DEG$3,
4152
+ eulerScratch.y * RAD2DEG$3,
4153
+ eulerScratch.z * RAD2DEG$3
3410
4154
  ];
3411
4155
  }
3412
4156
  pivot = null;
@@ -3431,7 +4175,15 @@ var CharacterController3D = class extends Node3D {
3431
4175
  function clamp(v, lo, hi) {
3432
4176
  return Math.min(hi, Math.max(lo, v));
3433
4177
  }
3434
- function worldOf(node) {
4178
+ function collectAreas(node, group, out) {
4179
+ if (node instanceof Area3D && node.isInGroup(group)) out.push(node);
4180
+ for (const child of node.children) collectAreas(child, group, out);
4181
+ }
4182
+ function collectWaters(node, out) {
4183
+ if (node instanceof Water3D) out.push(node);
4184
+ for (const child of node.children) collectWaters(child, out);
4185
+ }
4186
+ function worldOf$1(node) {
3435
4187
  let x = 0;
3436
4188
  let y = 0;
3437
4189
  let z = 0;
@@ -3460,6 +4212,344 @@ function groundXZ(node) {
3460
4212
  const world = worldPosition(node);
3461
4213
  return [world[0] ?? 0, world[2] ?? 0];
3462
4214
  }
4215
+ /** An XZ offset turned by the rotation that takes `qFrom` to `qNow` (quaternions `[x,y,z,w]`). */
4216
+ function rotateXZByDelta(qNow, qFrom, ox, oz) {
4217
+ const [fx, fy, fz, fw] = [
4218
+ -(qFrom[0] ?? 0),
4219
+ -(qFrom[1] ?? 0),
4220
+ -(qFrom[2] ?? 0),
4221
+ qFrom[3] ?? 1
4222
+ ];
4223
+ const [nx, ny, nz, nw] = [
4224
+ qNow[0] ?? 0,
4225
+ qNow[1] ?? 0,
4226
+ qNow[2] ?? 0,
4227
+ qNow[3] ?? 1
4228
+ ];
4229
+ const dx = nw * fx + nx * fw + ny * fz - nz * fy;
4230
+ const dy = nw * fy - nx * fz + ny * fw + nz * fx;
4231
+ const dz = nw * fz + nx * fy - ny * fx + nz * fw;
4232
+ const dw = nw * fw - nx * fx - ny * fy - nz * fz;
4233
+ const oy = 0;
4234
+ const cx = dy * oz - dz * oy;
4235
+ const cy = dz * ox - dx * oz;
4236
+ const cz = dx * oy - dy * ox;
4237
+ return [
4238
+ ox + 2 * (dw * cx + (dy * cz - dz * cy)),
4239
+ oy + 2 * (dw * cy + (dz * cx - dx * cz)),
4240
+ oz + 2 * (dw * cz + (dx * cy - dy * cx))
4241
+ ];
4242
+ }
4243
+ function hasPositionArray(n) {
4244
+ return Array.isArray(n.position);
4245
+ }
4246
+ //#endregion
4247
+ //#region src/3d/nodes/flock-3d.ts
4248
+ const RAD2DEG$2 = 180 / Math.PI;
4249
+ /**
4250
+ * A flock — gulls over a harbor, fish under a boat, bats out of a cave — as
4251
+ * ONE instanced mesh that flies its own instances.
4252
+ *
4253
+ * ```json
4254
+ * { "name": "Gulls", "type": "Flock3D",
4255
+ * "props": { "position": [0, 14, 0], "count": 40, "radius": 30, "height": 6,
4256
+ * "mesh": "box", "size": [0.5, 0.08, 0.3], "follow": "/root/Boat" } }
4257
+ * ```
4258
+ *
4259
+ * Boids (Reynolds 1987): each instance steers away from neighbours closer
4260
+ * than `separation`, toward the average heading of those within `sight`
4261
+ * (`alignment`) and toward their centre (`cohesion`), turns back inside an
4262
+ * ellipsoid `radius` wide and `height` tall around this node — or around the
4263
+ * node at `follow`, offset by this node's position — and never turns faster
4264
+ * than `turn`. Every instance flies at `speed`, +z forward, so a mesh that is
4265
+ * longer along z reads as a body with a beak.
4266
+ *
4267
+ * No physics bodies, no behaviour per bird: forty birds cost one draw call
4268
+ * and forty-squared distance checks a frame. Seeded from `engine.rng`, so a
4269
+ * seeded run flies the same flight. `boids()` answers headless.
4270
+ */
4271
+ var Flock3D = class extends InstancedMesh3D {
4272
+ static typeName = "Flock3D";
4273
+ static props = {
4274
+ ...InstancedMesh3D.props,
4275
+ /** How many. Live — a change reseeds the flock. */
4276
+ count: {
4277
+ default: 30,
4278
+ range: { min: 0 }
4279
+ },
4280
+ /** The bound they turn back from: half-width (m) around the centre… */
4281
+ radius: {
4282
+ default: 20,
4283
+ range: { min: .1 }
4284
+ },
4285
+ /** …and half-height (m). A flat ellipsoid keeps fish under the surface. */
4286
+ height: {
4287
+ default: 8,
4288
+ range: { min: .1 }
4289
+ },
4290
+ /** Every bird's pace (m/s). */
4291
+ speed: {
4292
+ default: 6,
4293
+ range: { min: 0 }
4294
+ },
4295
+ /** The sharpest turn (rad/s) — lower is lazier, gull-like. */
4296
+ turn: {
4297
+ default: 2.5,
4298
+ range: { min: 0 }
4299
+ },
4300
+ /** Closer than this (m) and two birds push apart. */
4301
+ separation: {
4302
+ default: 1.5,
4303
+ range: { min: 0 }
4304
+ },
4305
+ /** How far (m) a bird sees its neighbours for alignment and cohesion. */
4306
+ sight: {
4307
+ default: 5,
4308
+ range: { min: 0 }
4309
+ },
4310
+ /** Weight of matching the neighbours' heading (0 = none). */
4311
+ alignment: {
4312
+ default: 1,
4313
+ range: { min: 0 }
4314
+ },
4315
+ /** Weight of drifting toward the neighbours' centre (0 = none). */
4316
+ cohesion: {
4317
+ default: 1,
4318
+ range: { min: 0 }
4319
+ },
4320
+ /** A node the flock's centre follows, offset by this node's position; '' = this node. */
4321
+ follow: {
4322
+ default: "",
4323
+ nodePath: true
4324
+ }
4325
+ };
4326
+ count = 30;
4327
+ radius = 20;
4328
+ height = 8;
4329
+ speed = 6;
4330
+ turn = 2.5;
4331
+ separation = 1.5;
4332
+ sight = 5;
4333
+ alignment = 1;
4334
+ cohesion = 1;
4335
+ follow = "";
4336
+ px = new Float64Array(0);
4337
+ py = new Float64Array(0);
4338
+ pz = new Float64Array(0);
4339
+ vx = new Float64Array(0);
4340
+ vy = new Float64Array(0);
4341
+ vz = new Float64Array(0);
4342
+ rows = [];
4343
+ static validateJson(node) {
4344
+ InstancedMesh3D.validateJson(node);
4345
+ if (node.collider !== null) throw new IncantoError("BAD_FORMAT", `Flock3D '${node.name}': birds are not solid — a flock takes no "collider" (it flies; a static hull would stay where the first frame put it).`, { prop: "collider" });
4346
+ }
4347
+ /** Every bird right now, in world metres and m/s. */
4348
+ boids() {
4349
+ const [ox, oy, oz] = worldOf(this);
4350
+ const out = [];
4351
+ for (let i = 0; i < this.px.length; i++) out.push({
4352
+ x: (this.px[i] ?? 0) + ox,
4353
+ y: (this.py[i] ?? 0) + oy,
4354
+ z: (this.pz[i] ?? 0) + oz,
4355
+ vx: this.vx[i] ?? 0,
4356
+ vy: this.vy[i] ?? 0,
4357
+ vz: this.vz[i] ?? 0
4358
+ });
4359
+ return out;
4360
+ }
4361
+ update(dt) {
4362
+ const engine = this.tree?.engine;
4363
+ if (!engine) return;
4364
+ const n = Math.max(0, Math.floor(this.count));
4365
+ if (this.px.length !== n) this.seed(engine.rng, n);
4366
+ if (n === 0) {
4367
+ if (this.transforms.length !== 0) this.transforms = [];
4368
+ return;
4369
+ }
4370
+ const [cx, cy, cz] = this.centre();
4371
+ const step = Math.min(dt, 1 / 20);
4372
+ const sight2 = this.sight * this.sight;
4373
+ const sep2 = this.separation * this.separation;
4374
+ const maxTurn = this.turn * step;
4375
+ const { px, py, pz, vx, vy, vz } = this;
4376
+ for (let i = 0; i < n; i++) {
4377
+ const x = px[i] ?? 0;
4378
+ const y = py[i] ?? 0;
4379
+ const z = pz[i] ?? 0;
4380
+ let sx = 0;
4381
+ let sy = 0;
4382
+ let sz = 0;
4383
+ let ax = 0;
4384
+ let ay = 0;
4385
+ let az = 0;
4386
+ let mx = 0;
4387
+ let my = 0;
4388
+ let mz = 0;
4389
+ let seen = 0;
4390
+ for (let j = 0; j < n; j++) {
4391
+ if (j === i) continue;
4392
+ const dx = (px[j] ?? 0) - x;
4393
+ const dy = (py[j] ?? 0) - y;
4394
+ const dz = (pz[j] ?? 0) - z;
4395
+ const d2 = dx * dx + dy * dy + dz * dz;
4396
+ if (d2 > sight2) continue;
4397
+ seen += 1;
4398
+ ax += vx[j] ?? 0;
4399
+ ay += vy[j] ?? 0;
4400
+ az += vz[j] ?? 0;
4401
+ mx += dx;
4402
+ my += dy;
4403
+ mz += dz;
4404
+ if (d2 < sep2 && d2 > 1e-9) {
4405
+ const inv = 1 / d2;
4406
+ sx -= dx * inv;
4407
+ sy -= dy * inv;
4408
+ sz -= dz * inv;
4409
+ }
4410
+ }
4411
+ let fx = sx * this.speed * 2;
4412
+ let fy = sy * this.speed * 2;
4413
+ let fz = sz * this.speed * 2;
4414
+ if (seen > 0) {
4415
+ fx += (ax / seen - (vx[i] ?? 0)) * this.alignment / .5;
4416
+ fy += (ay / seen - (vy[i] ?? 0)) * this.alignment / .5;
4417
+ fz += (az / seen - (vz[i] ?? 0)) * this.alignment / .5;
4418
+ fx += mx / seen * this.cohesion * 2;
4419
+ fy += my / seen * this.cohesion * 2;
4420
+ fz += mz / seen * this.cohesion * 2;
4421
+ }
4422
+ const ex = (x - cx) / this.radius;
4423
+ const ey = (y - cy) / this.height;
4424
+ const ez = (z - cz) / this.radius;
4425
+ const m = Math.sqrt(ex * ex + ey * ey + ez * ez);
4426
+ if (m > .6) {
4427
+ const k = (m - .6) / .4 * this.speed * 8;
4428
+ const gx = ex / this.radius;
4429
+ const gy = ey / this.height;
4430
+ const gz = ez / this.radius;
4431
+ const g = Math.hypot(gx, gy, gz) || 1;
4432
+ fx -= gx / g * k;
4433
+ fy -= gy / g * k;
4434
+ fz -= gz / g * k;
4435
+ }
4436
+ let nx = (vx[i] ?? 0) + fx * step;
4437
+ let ny = (vy[i] ?? 0) + fy * step;
4438
+ let nz = (vz[i] ?? 0) + fz * step;
4439
+ const ov = Math.hypot(vx[i] ?? 0, vy[i] ?? 0, vz[i] ?? 0) || 1;
4440
+ const nv = Math.hypot(nx, ny, nz) || 1;
4441
+ const cos = ((vx[i] ?? 0) * nx + (vy[i] ?? 0) * ny + (vz[i] ?? 0) * nz) / (ov * nv);
4442
+ const angle = Math.acos(Math.max(-1, Math.min(1, cos)));
4443
+ if (angle > maxTurn && angle > 1e-6) {
4444
+ const t = maxTurn / angle;
4445
+ nx = (vx[i] ?? 0) / ov * (1 - t) + nx / nv * t;
4446
+ ny = (vy[i] ?? 0) / ov * (1 - t) + ny / nv * t;
4447
+ nz = (vz[i] ?? 0) / ov * (1 - t) + nz / nv * t;
4448
+ }
4449
+ const len = Math.hypot(nx, ny, nz) || 1;
4450
+ vx[i] = nx / len * this.speed;
4451
+ vy[i] = ny / len * this.speed;
4452
+ vz[i] = nz / len * this.speed;
4453
+ }
4454
+ for (let i = 0; i < n; i++) {
4455
+ px[i] = (px[i] ?? 0) + (vx[i] ?? 0) * step;
4456
+ py[i] = (py[i] ?? 0) + (vy[i] ?? 0) * step;
4457
+ pz[i] = (pz[i] ?? 0) + (vz[i] ?? 0) * step;
4458
+ let row = this.rows[i];
4459
+ if (!row) {
4460
+ row = [
4461
+ 0,
4462
+ 0,
4463
+ 0,
4464
+ 0,
4465
+ 1
4466
+ ];
4467
+ this.rows[i] = row;
4468
+ }
4469
+ row[0] = px[i] ?? 0;
4470
+ row[1] = py[i] ?? 0;
4471
+ row[2] = pz[i] ?? 0;
4472
+ row[3] = Math.atan2(vx[i] ?? 0, vz[i] ?? 0) * RAD2DEG$2;
4473
+ }
4474
+ this.transforms = this.rows;
4475
+ }
4476
+ /** The roam centre in this node's local frame: the followed node's offset, or the origin. */
4477
+ centre() {
4478
+ if (this.follow === "") return [
4479
+ 0,
4480
+ 0,
4481
+ 0
4482
+ ];
4483
+ const target = this.getNodeOrNull(this.follow);
4484
+ if (!(target instanceof Node3D)) return [
4485
+ 0,
4486
+ 0,
4487
+ 0
4488
+ ];
4489
+ const [tx, ty, tz] = worldOf(target);
4490
+ const [ox, oy, oz] = worldOf(this);
4491
+ const [lx, ly, lz] = [
4492
+ this.position[0] ?? 0,
4493
+ this.position[1] ?? 0,
4494
+ this.position[2] ?? 0
4495
+ ];
4496
+ return [
4497
+ tx - ox + lx,
4498
+ ty - oy + ly,
4499
+ tz - oz + lz
4500
+ ];
4501
+ }
4502
+ /** Scatter `n` birds through the bound with random headings — engine-seeded. */
4503
+ seed(rng, n) {
4504
+ this.px = new Float64Array(n);
4505
+ this.py = new Float64Array(n);
4506
+ this.pz = new Float64Array(n);
4507
+ this.vx = new Float64Array(n);
4508
+ this.vy = new Float64Array(n);
4509
+ this.vz = new Float64Array(n);
4510
+ this.rows.length = n;
4511
+ const [cx, cy, cz] = this.centre();
4512
+ for (let i = 0; i < n; i++) {
4513
+ const u = rng.next();
4514
+ const r = .6 * Math.cbrt(u);
4515
+ const theta = rng.next() * Math.PI * 2;
4516
+ const phi = Math.acos(2 * rng.next() - 1);
4517
+ this.px[i] = cx + r * this.radius * Math.sin(phi) * Math.cos(theta);
4518
+ this.py[i] = cy + r * this.height * Math.cos(phi);
4519
+ this.pz[i] = cz + r * this.radius * Math.sin(phi) * Math.sin(theta);
4520
+ const h = rng.next() * Math.PI * 2;
4521
+ const pitch = (rng.next() - .5) * .4;
4522
+ this.vx[i] = Math.cos(pitch) * Math.sin(h) * this.speed;
4523
+ this.vy[i] = Math.sin(pitch) * this.speed;
4524
+ this.vz[i] = Math.cos(pitch) * Math.cos(h) * this.speed;
4525
+ this.rows[i] = [
4526
+ this.px[i] ?? 0,
4527
+ this.py[i] ?? 0,
4528
+ this.pz[i] ?? 0,
4529
+ h * RAD2DEG$2,
4530
+ 1
4531
+ ];
4532
+ }
4533
+ this.transforms = this.rows;
4534
+ }
4535
+ };
4536
+ /** Translation-only world position (the engine's physics rule). */
4537
+ function worldOf(node) {
4538
+ let x = 0;
4539
+ let y = 0;
4540
+ let z = 0;
4541
+ for (let n = node; n; n = n.parent) {
4542
+ if (!(n instanceof Node3D)) break;
4543
+ x += n.position[0] ?? 0;
4544
+ y += n.position[1] ?? 0;
4545
+ z += n.position[2] ?? 0;
4546
+ }
4547
+ return [
4548
+ x,
4549
+ y,
4550
+ z
4551
+ ];
4552
+ }
3463
4553
  //#endregion
3464
4554
  //#region src/3d/vegetation/grass-placement.ts
3465
4555
  const NOISE_SCALE = .15;
@@ -7625,7 +8715,19 @@ var Particles3D = class Particles3D extends Node3D {
7625
8715
  options: ["add", "normal"]
7626
8716
  },
7627
8717
  depthTest: { default: true },
7628
- maxParticles: { default: 256 }
8718
+ maxParticles: { default: 256 },
8719
+ /** Half-extents (m) of a box the particles are born in, around the node — a cloud, a dusty field; `[0,0,0]` = a point. */
8720
+ emitBox: { default: [
8721
+ 0,
8722
+ 0,
8723
+ 0
8724
+ ] },
8725
+ /** A steady velocity (px/s, y+ = down like `gravity`) every particle is born with — wind on smoke. */
8726
+ drift: { default: [
8727
+ 0,
8728
+ 0,
8729
+ 0
8730
+ ] }
7629
8731
  };
7630
8732
  preset = "custom";
7631
8733
  emitting = true;
@@ -7682,6 +8784,20 @@ var Particles3D = class Particles3D extends Node3D {
7682
8784
  });
7683
8785
  }
7684
8786
  sim = null;
8787
+ emitBox = [
8788
+ 0,
8789
+ 0,
8790
+ 0
8791
+ ];
8792
+ drift = [
8793
+ 0,
8794
+ 0,
8795
+ 0
8796
+ ];
8797
+ /** @internal Forget the sim so the next update rebuilds it from the props (a preset or kind that changed). */
8798
+ _rebuildSim() {
8799
+ this.sim = null;
8800
+ }
7685
8801
  bursted = false;
7686
8802
  finishedEmitted = false;
7687
8803
  /** @internal */
@@ -7702,7 +8818,17 @@ var Particles3D = class Particles3D extends Node3D {
7702
8818
  ],
7703
8819
  drag: this.drag,
7704
8820
  maxParticles: this.maxParticles,
7705
- spreadZ: true
8821
+ spreadZ: true,
8822
+ emitBox: [
8823
+ this.emitBox[0] ?? 0,
8824
+ this.emitBox[1] ?? 0,
8825
+ this.emitBox[2] ?? 0
8826
+ ],
8827
+ drift: [
8828
+ (this.drift[0] ?? 0) * PX_TO_M,
8829
+ (this.drift[1] ?? 0) * PX_TO_M,
8830
+ (this.drift[2] ?? 0) * PX_TO_M
8831
+ ]
7706
8832
  }, rng);
7707
8833
  }
7708
8834
  return this.sim;
@@ -7824,9 +8950,24 @@ var Particles3D = class Particles3D extends Node3D {
7824
8950
  }
7825
8951
  return this.paletteCache;
7826
8952
  }
8953
+ /**
8954
+ * The stamp every particle is drawn with — a soft round dot.
8955
+ *
8956
+ * A subclass overrides it to look like the thing it IS: `Weather3D`'s rain is
8957
+ * a vertical STREAK, because 2,600 round dots read as a dirty lens and not as
8958
+ * a downpour (measured in a browser at intensity 1). The material is rebuilt
8959
+ * when this changes, so a node may return a different stamp over its life.
8960
+ */
8961
+ sprite() {
8962
+ return softSprite();
8963
+ }
8964
+ /** The stamp the current material was built with, so a change can rebuild it. */
8965
+ builtSprite = null;
7827
8966
  syncParticles() {
7828
8967
  const sim = this._ensureSim();
7829
- if (!this.points || (this.positions?.length ?? 0) !== this.maxParticles * 3) {
8968
+ const stamp = this.sprite();
8969
+ if (!this.points || (this.positions?.length ?? 0) !== this.maxParticles * 3 || this.builtSprite !== stamp) {
8970
+ this.builtSprite = stamp;
7830
8971
  this.points?.removeFromParent();
7831
8972
  this.positions = new Float32Array(this.maxParticles * 3);
7832
8973
  this.colors = new Float32Array(this.maxParticles * 4);
@@ -7835,7 +8976,7 @@ var Particles3D = class Particles3D extends Node3D {
7835
8976
  geometry.setAttribute("color", new BufferAttribute(this.colors, 4));
7836
8977
  const material = new PointsMaterial({
7837
8978
  size: (this.sizeStart + this.sizeEnd) / 2 * PX_TO_M,
7838
- map: softSprite(),
8979
+ map: this.sprite(),
7839
8980
  vertexColors: true,
7840
8981
  transparent: true,
7841
8982
  depthWrite: false,
@@ -7942,6 +9083,356 @@ function softSprite() {
7942
9083
  return tex;
7943
9084
  }
7944
9085
  //#endregion
9086
+ //#region src/3d/nodes/ragdoll-3d.ts
9087
+ const RAD2DEG$1 = 180 / Math.PI;
9088
+ const DEG2RAD = Math.PI / 180;
9089
+ /**
9090
+ * The Mixamo rig every agent8 character wears, as ten capsules. Parents
9091
+ * come before children so a joint's target is already in the tree. A limb
9092
+ * whose `to` bone is missing is skipped — except the head, which becomes a
9093
+ * 0.2 m ball when the rig has no `HeadTop_End`.
9094
+ */
9095
+ const RIG = [
9096
+ {
9097
+ name: "Torso",
9098
+ from: "Hips",
9099
+ to: "Neck",
9100
+ radius: .14,
9101
+ share: .32,
9102
+ parent: null
9103
+ },
9104
+ {
9105
+ name: "Head",
9106
+ from: "Head",
9107
+ to: "HeadTop_End",
9108
+ radius: .1,
9109
+ share: .08,
9110
+ parent: "Torso"
9111
+ },
9112
+ {
9113
+ name: "LeftUpperArm",
9114
+ from: "LeftArm",
9115
+ to: "LeftForeArm",
9116
+ radius: .05,
9117
+ share: .05,
9118
+ parent: "Torso"
9119
+ },
9120
+ {
9121
+ name: "LeftForearm",
9122
+ from: "LeftForeArm",
9123
+ to: "LeftHand",
9124
+ radius: .045,
9125
+ share: .04,
9126
+ parent: "LeftUpperArm"
9127
+ },
9128
+ {
9129
+ name: "RightUpperArm",
9130
+ from: "RightArm",
9131
+ to: "RightForeArm",
9132
+ radius: .05,
9133
+ share: .05,
9134
+ parent: "Torso"
9135
+ },
9136
+ {
9137
+ name: "RightForearm",
9138
+ from: "RightForeArm",
9139
+ to: "RightHand",
9140
+ radius: .045,
9141
+ share: .04,
9142
+ parent: "RightUpperArm"
9143
+ },
9144
+ {
9145
+ name: "LeftThigh",
9146
+ from: "LeftUpLeg",
9147
+ to: "LeftLeg",
9148
+ radius: .08,
9149
+ share: .12,
9150
+ parent: "Torso"
9151
+ },
9152
+ {
9153
+ name: "LeftShin",
9154
+ from: "LeftLeg",
9155
+ to: "LeftFoot",
9156
+ radius: .06,
9157
+ share: .07,
9158
+ parent: "LeftThigh"
9159
+ },
9160
+ {
9161
+ name: "RightThigh",
9162
+ from: "RightUpLeg",
9163
+ to: "RightLeg",
9164
+ radius: .08,
9165
+ share: .12,
9166
+ parent: "Torso"
9167
+ },
9168
+ {
9169
+ name: "RightShin",
9170
+ from: "RightLeg",
9171
+ to: "RightFoot",
9172
+ radius: .06,
9173
+ share: .07,
9174
+ parent: "RightThigh"
9175
+ }
9176
+ ];
9177
+ const scratchM = new Matrix4();
9178
+ const scratchM2 = new Matrix4();
9179
+ const scratchQ = new Quaternion();
9180
+ const scratchE = new Euler();
9181
+ const scratchV = new Vector3();
9182
+ const scratchS = new Vector3(1, 1, 1);
9183
+ /**
9184
+ * A ragdoll for a skinned model — the death that falls.
9185
+ *
9186
+ * ```json
9187
+ * { "name": "Body", "type": "CharacterBody3D", "children": [
9188
+ * { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$base" } },
9189
+ * { "name": "Ragdoll", "type": "Ragdoll3D", "props": { "target": "../Skin", "lifetime": 8 } }
9190
+ * ] }
9191
+ * ```
9192
+ *
9193
+ * Idle it is nothing. `activate()` reads the skeleton at `target` (Mixamo
9194
+ * names, forgiving lookup), builds a capsule `RigidBody3D` per limb where the
9195
+ * bones are, joins each to its parent with a spherical `Joint3D`, gives them
9196
+ * the velocity of the body they fell out of, and from then on writes the
9197
+ * bones' world matrices from the limbs every frame — the model crumples,
9198
+ * slides, hangs off the ledge. Wire it from `Health.died`; turn the body's
9199
+ * collider and its controller off beside it. `reset()` puts the skeleton
9200
+ * back under the animation; `lifetime` does that on its own.
9201
+ *
9202
+ * Headless there is no skeleton: `activate(pose)` takes bone positions, so a
9203
+ * test can ask the same questions the game will.
9204
+ */
9205
+ var Ragdoll3D = class extends Node3D {
9206
+ static typeName = "Ragdoll3D";
9207
+ static props = {
9208
+ /** The ModelInstance3D whose skeleton this is. */
9209
+ target: {
9210
+ default: "../Skin",
9211
+ nodePath: true
9212
+ },
9213
+ /** The whole body's mass (kg), shared out by limb. */
9214
+ mass: {
9215
+ default: 70,
9216
+ range: { min: 0 }
9217
+ },
9218
+ /** Angular damping on every limb — how quickly the flailing dies. */
9219
+ damping: {
9220
+ default: 3,
9221
+ range: { min: 0 }
9222
+ },
9223
+ /** Seconds before the ragdoll frees itself and hands the skeleton back; 0 = never. */
9224
+ lifetime: {
9225
+ default: 0,
9226
+ range: { min: 0 }
9227
+ },
9228
+ /** Limbs collide with each other (adjacent ones never do). */
9229
+ collide: { default: true }
9230
+ };
9231
+ static signals = ["activated", "reset"];
9232
+ target = "../Skin";
9233
+ mass = 70;
9234
+ damping = 3;
9235
+ lifetime = 0;
9236
+ collide = true;
9237
+ /** Read-only: built and driving the bones. */
9238
+ active = false;
9239
+ driven = [];
9240
+ limbNodes = [];
9241
+ left = 0;
9242
+ /** Every limb built right now, empty while idle. */
9243
+ limbs() {
9244
+ return this.limbNodes.slice();
9245
+ }
9246
+ /**
9247
+ * Build the ragdoll where the skeleton is (or where `pose` says the bones
9248
+ * are, headless). Returns false when there is nothing to build from.
9249
+ */
9250
+ activate(pose = void 0) {
9251
+ if (this.active) return true;
9252
+ const root = this.getRoot();
9253
+ const model = this.getNodeOrNull(this.target);
9254
+ const bones = /* @__PURE__ */ new Map();
9255
+ const points = {};
9256
+ if (pose) for (const [k, v] of Object.entries(pose)) points[k] = v;
9257
+ else if (model && typeof model.findBone === "function") for (const spec of RIG) for (const name of [spec.from, spec.to]) {
9258
+ if (points[name]) continue;
9259
+ const bone = model.findBone(name);
9260
+ if (!bone) continue;
9261
+ bone.updateWorldMatrix(true, false);
9262
+ scratchV.setFromMatrixPosition(bone.matrixWorld);
9263
+ points[name] = [
9264
+ scratchV.x,
9265
+ scratchV.y,
9266
+ scratchV.z
9267
+ ];
9268
+ bones.set(name, bone);
9269
+ }
9270
+ if (!points.Hips) {
9271
+ diagnose(this.tree?.engine, "warn", `[incanto] Ragdoll3D '${this.getPath()}': nothing to build from — '${this.target}' has no mounted skeleton (or no Hips bone), and no pose was given.`);
9272
+ return false;
9273
+ }
9274
+ const velocity = this.hostVelocity();
9275
+ const built = /* @__PURE__ */ new Map();
9276
+ for (const spec of RIG) {
9277
+ const from = points[spec.from];
9278
+ if (!from) continue;
9279
+ let to = points[spec.to];
9280
+ if (!to) {
9281
+ if (spec.name !== "Head") continue;
9282
+ to = [
9283
+ from[0],
9284
+ from[1] + .2,
9285
+ from[2]
9286
+ ];
9287
+ }
9288
+ const parent = spec.parent ? built.get(spec.parent) : null;
9289
+ if (spec.parent && !parent) continue;
9290
+ const dir = new Vector3(to[0] - from[0], to[1] - from[1], to[2] - from[2]);
9291
+ const len = Math.max(dir.length(), .05);
9292
+ dir.normalize();
9293
+ const centre = new Vector3(from[0] + dir.x * (len / 2), from[1] + dir.y * (len / 2), from[2] + dir.z * (len / 2));
9294
+ const rz = Math.asin(Math.max(-1, Math.min(1, -dir.x)));
9295
+ const cz = Math.cos(rz);
9296
+ const rx = cz > 1e-4 ? Math.atan2(dir.z / cz, dir.y / cz) : 0;
9297
+ const quat = new Quaternion().setFromEuler(new Euler(rx, 0, rz, "XYZ"));
9298
+ const radius = Math.min(spec.radius, len / 2 - .005);
9299
+ const json = {
9300
+ name: `${this.parent?.name ?? "Ragdoll"}${spec.name}`,
9301
+ type: "RigidBody3D",
9302
+ groups: ["ragdoll"],
9303
+ props: {
9304
+ position: [
9305
+ centre.x,
9306
+ centre.y,
9307
+ centre.z
9308
+ ],
9309
+ rotation: [
9310
+ rx * RAD2DEG$1,
9311
+ 0,
9312
+ rz * RAD2DEG$1
9313
+ ],
9314
+ mass: Math.max(.5, this.mass * spec.share),
9315
+ angularDamping: this.damping,
9316
+ linearDamping: .3,
9317
+ friction: .8,
9318
+ collider: {
9319
+ shape: "capsule",
9320
+ radius,
9321
+ height: Math.max(.02, len - 2 * radius)
9322
+ },
9323
+ linearVelocity: velocity
9324
+ }
9325
+ };
9326
+ if (parent) {
9327
+ const local = new Vector3(from[0], from[1], from[2]).sub(parent.centre);
9328
+ local.applyQuaternion(parent.quat.clone().invert());
9329
+ const children = [];
9330
+ json.children = children;
9331
+ children.push({
9332
+ name: "Joint",
9333
+ type: "Joint3D",
9334
+ props: {
9335
+ type: "spherical",
9336
+ target: parent.node.getPath(),
9337
+ anchor: [
9338
+ 0,
9339
+ -len / 2,
9340
+ 0
9341
+ ],
9342
+ targetAnchor: [
9343
+ local.x,
9344
+ local.y,
9345
+ local.z
9346
+ ],
9347
+ collide: false
9348
+ }
9349
+ });
9350
+ }
9351
+ const node = buildNodeJson(json, rebuildOptions(this));
9352
+ root.addChild(node);
9353
+ built.set(spec.name, {
9354
+ node,
9355
+ centre,
9356
+ quat,
9357
+ len
9358
+ });
9359
+ this.limbNodes.push({
9360
+ name: spec.name,
9361
+ body: node
9362
+ });
9363
+ const bone = bones.get(spec.from) ?? null;
9364
+ const offset = new Matrix4();
9365
+ if (bone) {
9366
+ scratchM.compose(centre, quat, scratchS);
9367
+ offset.copy(scratchM).invert().multiply(bone.matrixWorld);
9368
+ bone.matrixWorldAutoUpdate = false;
9369
+ }
9370
+ this.driven.push({
9371
+ body: node,
9372
+ bone,
9373
+ offset
9374
+ });
9375
+ }
9376
+ if (this.limbNodes.length === 0) return false;
9377
+ this.active = true;
9378
+ this.left = this.lifetime;
9379
+ this.emit("activated");
9380
+ return true;
9381
+ }
9382
+ /** Free the limbs and hand the skeleton back to the animation. */
9383
+ reset() {
9384
+ if (!this.active) return;
9385
+ for (const d of this.driven) {
9386
+ if (d.bone) d.bone.matrixWorldAutoUpdate = true;
9387
+ if (d.body.tree !== null) d.body.queueFree();
9388
+ }
9389
+ this.driven = [];
9390
+ this.limbNodes = [];
9391
+ this.active = false;
9392
+ this.emit("reset");
9393
+ }
9394
+ update(dt) {
9395
+ if (!this.active) return;
9396
+ if (this.lifetime > 0) {
9397
+ this.left -= dt;
9398
+ if (this.left <= 0) {
9399
+ this.reset();
9400
+ return;
9401
+ }
9402
+ }
9403
+ for (const d of this.driven) {
9404
+ if (!d.bone) continue;
9405
+ const p = d.body.position;
9406
+ const r = d.body.rotation;
9407
+ scratchV.set(p[0] ?? 0, p[1] ?? 0, p[2] ?? 0);
9408
+ scratchQ.setFromEuler(scratchE.set((r[0] ?? 0) * DEG2RAD, (r[1] ?? 0) * DEG2RAD, (r[2] ?? 0) * DEG2RAD, "XYZ"));
9409
+ scratchM.compose(scratchV, scratchQ, scratchS);
9410
+ scratchM2.copy(scratchM).multiply(d.offset);
9411
+ d.bone.matrixWorld.copy(scratchM2);
9412
+ }
9413
+ }
9414
+ free() {
9415
+ this.reset();
9416
+ super.free();
9417
+ }
9418
+ /** The velocity of the body this ragdoll falls out of — the first body up the tree. */
9419
+ hostVelocity() {
9420
+ for (let n = this.parent; n; n = n.parent) {
9421
+ const v = n.linearVelocity ?? n.velocity;
9422
+ if (Array.isArray(v)) return [
9423
+ v[0] ?? 0,
9424
+ v[1] ?? 0,
9425
+ v[2] ?? 0
9426
+ ];
9427
+ }
9428
+ return [
9429
+ 0,
9430
+ 0,
9431
+ 0
9432
+ ];
9433
+ }
9434
+ };
9435
+ //#endregion
7945
9436
  //#region src/3d/water/river.ts
7946
9437
  /**
7947
9438
  * Self-carved bed. Sized off the WATER COLUMN, never off the width: a channel's
@@ -9386,7 +10877,7 @@ var River3D = class River3D extends Node3D {
9386
10877
  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" });
9387
10878
  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" });
9388
10879
  r.path.forEach((p, i) => {
9389
- if (!Array.isArray(p) || p.length < 2 || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path[${i}] must be [x, z] with finite numbers, got ${JSON.stringify(p)}.`, { prop: "path" });
10880
+ if (!isCoursePoint(p)) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path[${i}] must be ${COURSE_SHAPE}, got ${JSON.stringify(p)}.`, { prop: "path" });
9390
10881
  });
9391
10882
  if (!(typeof r.width === "number" && r.width > 0)) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' width must be a positive number of meters, got ${JSON.stringify(r.width)}.`, { prop: "width" });
9392
10883
  if (!Array.isArray(r.widths) || r.widths.some((w) => !(typeof w === "number" && w > 0))) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' widths must be a list of positive meters (source→mouth), got ${JSON.stringify(r.widths)}.`, { prop: "widths" });
@@ -9420,8 +10911,14 @@ var River3D = class River3D extends Node3D {
9420
10911
  onExitTree() {
9421
10912
  findDrapeTerrain(this, this.terrain)?._setExternalChannel(this, null);
9422
10913
  }
10914
+ /** Said once when the node has no course to be a river along. */
10915
+ warnedNoPath = false;
9423
10916
  update(dt) {
9424
10917
  this.time += dt;
10918
+ if (!this.warnedNoPath && this.path.length === 0) {
10919
+ this.warnedNoPath = true;
10920
+ this.diagnose("warn", `[incanto] River3D '${this.name}' has no \`path\`, so there is no channel: it draws nothing, carves nothing and pushes nothing, whatever \`width\`, \`flowSpeed\` and \`flowForce\` say. Give it at least two [x, z] control points (source → mouth), in the node's own local frame.`);
10921
+ }
9425
10922
  this.ensureCourse();
9426
10923
  this.syncSpray();
9427
10924
  if (this.flowForce > 0 && this.rings.length > 1) this.sweepBodies(dt);
@@ -9552,7 +11049,7 @@ var River3D = class River3D extends Node3D {
9552
11049
  const [tx, , tz] = worldXZY(terrain);
9553
11050
  const hm = terrain._heightmap();
9554
11051
  terrain._setExternalChannel(this, riverCarveChannels({
9555
- path: this.path.map((p) => [(p[0] ?? 0) + wx - tx, (p[1] ?? 0) + wz - tz]),
11052
+ path: course(this.path).map((p) => [p[0] + wx - tx, p[1] + wz - tz]),
9556
11053
  widths: this.widthProfile(),
9557
11054
  depth: this.depth,
9558
11055
  cell: (terrain.size[0] ?? 1) / Math.max(1, hm.segsX),
@@ -9597,7 +11094,7 @@ var River3D = class River3D extends Node3D {
9597
11094
  this.fitted = terrain !== null;
9598
11095
  const widths = this.widthProfile();
9599
11096
  this.rings = buildRiverRings({
9600
- path: this.path,
11097
+ path: course(this.path),
9601
11098
  widths,
9602
11099
  depth: this.depth,
9603
11100
  flowSpeed: this.flowSpeed,
@@ -11615,6 +13112,8 @@ function paintVerticalGradient(geometry, base, darkest, brightest, rng) {
11615
13112
  }
11616
13113
  //#endregion
11617
13114
  //#region src/3d/nodes/tree-3d.ts
13115
+ /** How many times a wet draw is re-rolled before the tree is planted anyway. */
13116
+ const DRY_TRIES = 8;
11618
13117
  /** A whole forest in one node is still ≤6 draw calls — but keep it sane. */
11619
13118
  const MAX_TREE_COUNT = 500;
11620
13119
  /** Whole-grove triangle ceiling: count × tris-per-tree must stay under this. */
@@ -11770,6 +13269,7 @@ var Tree3D = class Tree3D extends Node3D {
11770
13269
  default: null,
11771
13270
  kinds: ["null", "boolean"]
11772
13271
  },
13272
+ avoidWater: { default: true },
11773
13273
  terrain: {
11774
13274
  default: "",
11775
13275
  nodePath: true
@@ -11809,6 +13309,19 @@ var Tree3D = class Tree3D extends Node3D {
11809
13309
  * it (keeps retrying until a terrain appears), false disables draping.
11810
13310
  */
11811
13311
  drape = null;
13312
+ /**
13313
+ * Keep trunks out of standing and running water (default true) — a valley a
13314
+ * rill runs through has a grove on its banks, not trees in the current.
13315
+ *
13316
+ * `Foliage3D` and `Flowers3D` have had this since they were written, with the
13317
+ * same default and the same reason; the TREE — the tallest thing in a
13318
+ * landscape and the one that most obviously does not grow midstream — did
13319
+ * not. Measured on a river-valley game: two of twenty-two trunks stood in the
13320
+ * rill.
13321
+ *
13322
+ * `false` is the mangrove, the cypress swamp and the flooded forest.
13323
+ */
13324
+ avoidWater = true;
11812
13325
  /** Node path to the Terrain3D to drape onto ('' = auto-find the first one). */
11813
13326
  terrain = "";
11814
13327
  /** Resolved drape target + the grove's world position, set per rebuild. */
@@ -12062,13 +13575,45 @@ var Tree3D = class Tree3D extends Node3D {
12062
13575
  }, void 0, this.noteTextureError);
12063
13576
  }
12064
13577
  /** Per-instance placement + jitter — ONE rng draw order for both paths. */
13578
+ /**
13579
+ * Where every trunk of this grove stands, in WORLD coordinates.
13580
+ *
13581
+ * `drape` is the prop that puts a grove on the ground it grows from, and
13582
+ * nothing could ask whether it had. `UiMinimap.markers()` and
13583
+ * `HudLayer.focusables()` are the same accessor for the same reason: a
13584
+ * feature whose effect no harness can read is a feature nobody checks.
13585
+ *
13586
+ * Recomputed from the seed rather than read back off the InstancedMesh, so it
13587
+ * answers headless too — where there is no mesh at all.
13588
+ */
13589
+ trunks() {
13590
+ this.resolveDrape();
13591
+ const out = [];
13592
+ const [wx, wy, wz] = worldXZY(this);
13593
+ this.scatter(new Rng(this.seed), (_i, matrix) => {
13594
+ positionScratch.setFromMatrixPosition(matrix);
13595
+ out.push([
13596
+ positionScratch.x + wx,
13597
+ positionScratch.y + wy,
13598
+ positionScratch.z + wz
13599
+ ]);
13600
+ });
13601
+ return out;
13602
+ }
12065
13603
  scatter(rng, place) {
12066
13604
  const halfX = (this.area[0] ?? 0) / 2;
12067
13605
  const halfZ = (this.area[1] ?? 0) / 2;
13606
+ const wet = this.avoidWater ? collectWetSurfaces(this) : [];
13607
+ const [wx, wy, wz] = worldXZY(this);
12068
13608
  for (let i = 0; i < this.count; i++) {
12069
- const lx = this.count === 1 ? 0 : rng.range(-halfX, halfX);
12070
- const lz = this.count === 1 ? 0 : rng.range(-halfZ, halfZ);
12071
- positionScratch.set(lx, this.drape ? this.bedY(lx, lz) : 0, lz);
13609
+ let lx = this.count === 1 ? 0 : rng.range(-halfX, halfX);
13610
+ let lz = this.count === 1 ? 0 : rng.range(-halfZ, halfZ);
13611
+ if (wet.length > 0) for (let tries = 0; tries < DRY_TRIES; tries++) {
13612
+ if (!isUnderWater(wet, lx + wx, lz + wz, this.bedY(lx, lz) + wy)) break;
13613
+ lx = rng.range(-halfX, halfX);
13614
+ lz = rng.range(-halfZ, halfZ);
13615
+ }
13616
+ positionScratch.set(lx, this.bedY(lx, lz), lz);
12072
13617
  quaternionScratch.setFromAxisAngle(Y_AXIS, rng.range(0, Math.PI * 2));
12073
13618
  const h = this.height * rng.range(HEIGHT_JITTER[0], HEIGHT_JITTER[1]);
12074
13619
  const girth = h * rng.range(.9, 1.1);
@@ -12244,6 +13789,195 @@ const quaternionScratch = new Quaternion();
12244
13789
  const scaleScratch = new Vector3();
12245
13790
  const colorScratch = new Color();
12246
13791
  //#endregion
13792
+ //#region src/3d/nodes/vehicle-3d.ts
13793
+ const DEG = Math.PI / 180;
13794
+ const RAD2DEG = 180 / Math.PI;
13795
+ /**
13796
+ * A car in one node — Rapier's raycast vehicle under a `RigidBody3D` chassis.
13797
+ *
13798
+ * ```json
13799
+ * { "name": "Car", "type": "RigidBody3D",
13800
+ * "props": { "mass": 800, "collider": { "shape": "box", "size": [1.8, 0.6, 4] } },
13801
+ * "children": [
13802
+ * { "name": "Drive", "type": "Vehicle3D", "props": { "wheels": [
13803
+ * { "position": [-0.8, -0.1, 1.4], "steer": true },
13804
+ * { "position": [ 0.8, -0.1, 1.4], "steer": true },
13805
+ * { "position": [-0.8, -0.1, -1.4], "drive": true },
13806
+ * { "position": [ 0.8, -0.1, -1.4], "drive": true } ] } },
13807
+ * { "name": "Wheel0", "type": "Node3D", "props": { "position": [-0.8, -0.1, 1.4] },
13808
+ * "children": [ { "name": "Hub", "type": "Node3D", "children": [ …the tyre mesh… ] } ] }
13809
+ * ] }
13810
+ * ```
13811
+ *
13812
+ * The wheels are springs that never touch anything (a ray each), so a car
13813
+ * cannot catch a kerb on a wheel body or disagree with its axle; the throttle,
13814
+ * steer and brake come from the `move` vector and the `brake` button (or from
13815
+ * code, with `moveAction: ""`). Forward is +z, the +Z-forward rule. A child
13816
+ * named `Wheel<i>` is posed every step — it hangs on its spring and turns to
13817
+ * steer — and its child `Hub` spins as the wheel rolls.
13818
+ */
13819
+ var Vehicle3D = class extends Node3D {
13820
+ static typeName = "Vehicle3D";
13821
+ static props = {
13822
+ /** `[{ position, radius?, rest?, steer?, drive? }]` — at least one; defaults radius 0.35, rest 0.3. */
13823
+ wheels: { default: [] },
13824
+ /** Spring stiffness (N/m per kg-ish — 20 soft, 30 a car, 60 a kart). */
13825
+ suspensionStiffness: { default: 30 },
13826
+ suspensionCompression: { default: 2.3 },
13827
+ suspensionRelaxation: { default: 3.5 },
13828
+ /** How far a spring may compress from rest (m). */
13829
+ maxTravel: { default: .3 },
13830
+ /** Tyre grip along the wheel (higher = less spin-out). */
13831
+ friction: { default: 2.5 },
13832
+ /** Tyre grip across the wheel (lower = more drift). */
13833
+ sideFriction: { default: 1 },
13834
+ /** Full lock, degrees. */
13835
+ maxSteerDeg: { default: 32 },
13836
+ /** Force on each driven wheel at full throttle (N). */
13837
+ engineForce: { default: 1500 },
13838
+ /** Brake force on every wheel at full brake. */
13839
+ brakeForce: { default: 40 },
13840
+ /** The stick: up = throttle, down = reverse, left/right = steer. '' = code writes `throttle`/`steer`. */
13841
+ moveAction: { default: "move" },
13842
+ /** Held = full brake. */
13843
+ brakeAction: { default: "brake" },
13844
+ /**
13845
+ * Off = parked: the throttle and steering read nothing and the brake is
13846
+ * held, while the suspension still settles and the wheels still pose.
13847
+ * The switch a game flips while nobody is behind the wheel — both the
13848
+ * car and the character read `move`, so without it one press moves both.
13849
+ */
13850
+ enabled: { default: true }
13851
+ };
13852
+ wheels = [];
13853
+ suspensionStiffness = 30;
13854
+ suspensionCompression = 2.3;
13855
+ suspensionRelaxation = 3.5;
13856
+ maxTravel = .3;
13857
+ friction = 2.5;
13858
+ sideFriction = 1;
13859
+ maxSteerDeg = 32;
13860
+ engineForce = 1500;
13861
+ brakeForce = 40;
13862
+ moveAction = "move";
13863
+ brakeAction = "brake";
13864
+ enabled = true;
13865
+ /** Live inputs, −1..1 (throttle, steer) and 0..1 (brake) — the actions write these, or your code does. */
13866
+ throttle = 0;
13867
+ steer = 0;
13868
+ brake = 0;
13869
+ /** Forward speed, m/s (negative in reverse). */
13870
+ speed = 0;
13871
+ /** @internal Written by the physics adapter each step. */
13872
+ _states = [];
13873
+ reportedActions = /* @__PURE__ */ new Set();
13874
+ /** What wheel `i` is doing. */
13875
+ wheel(i) {
13876
+ return this._states[i] ?? {
13877
+ contact: false,
13878
+ suspension: this.specs()[i]?.rest ?? .3,
13879
+ rotation: 0,
13880
+ steering: 0
13881
+ };
13882
+ }
13883
+ /** The wheels with every default filled in. */
13884
+ specs() {
13885
+ return this.wheels.map((w) => {
13886
+ const raw = w;
13887
+ return {
13888
+ position: [
13889
+ raw.position?.[0] ?? 0,
13890
+ raw.position?.[1] ?? 0,
13891
+ raw.position?.[2] ?? 0
13892
+ ],
13893
+ radius: raw.radius ?? .35,
13894
+ rest: raw.rest ?? .3,
13895
+ steer: raw.steer ?? false,
13896
+ drive: raw.drive ?? false
13897
+ };
13898
+ });
13899
+ }
13900
+ get body() {
13901
+ const parent = this.parent;
13902
+ if (!(parent instanceof RigidBody3D)) throw new IncantoError("TREE_VIOLATION", `Vehicle3D '${this.name}' must be a child of a RigidBody3D — the chassis (parent is '${parent?.name ?? "none"}').`);
13903
+ return parent;
13904
+ }
13905
+ onEnterTree() {
13906
+ this.body;
13907
+ if (!Array.isArray(this.wheels) || this.wheels.length === 0) throw new IncantoError("BAD_FORMAT", `Vehicle3D '${this.getPath()}': "wheels" must list at least one wheel — { "position": [x, y, z], "steer"?, "drive"? }.`);
13908
+ this.wheels.forEach((w, i) => {
13909
+ const p = w.position;
13910
+ if (!Array.isArray(p) || p.length !== 3 || p.some((n) => typeof n !== "number" || !Number.isFinite(n))) throw new IncantoError("BAD_FORMAT", `Vehicle3D '${this.getPath()}': wheel ${i} needs a "position": [x, y, z] in the chassis's frame.`);
13911
+ });
13912
+ }
13913
+ onExitTree() {
13914
+ this.body._physics3d?._freeVehicle(this);
13915
+ }
13916
+ fixedUpdate(dt) {
13917
+ const input = this.tree?.engine?.input;
13918
+ const physics = this.body._physics3d;
13919
+ if (input && this.moveAction !== "") {
13920
+ const v = tolerateUnknownAction({
13921
+ engine: this.tree?.engine,
13922
+ who: this.getPath(),
13923
+ reported: this.reportedActions,
13924
+ consequence: "The car will not move.",
13925
+ authored: this.moveAction !== "move"
13926
+ }, this.moveAction, () => input.getVector(this.moveAction), {
13927
+ x: 0,
13928
+ y: 0
13929
+ });
13930
+ this.throttle = -v.y;
13931
+ this.steer = v.x;
13932
+ this.brake = tolerateUnknownAction({
13933
+ engine: this.tree?.engine,
13934
+ who: this.getPath(),
13935
+ reported: this.reportedActions,
13936
+ consequence: "The car will never brake.",
13937
+ authored: this.brakeAction !== "brake"
13938
+ }, this.brakeAction, () => input.isPressed(this.brakeAction) ? 1 : 0, 0);
13939
+ }
13940
+ if (!this.enabled) {
13941
+ this.throttle = 0;
13942
+ this.steer = 0;
13943
+ this.brake = 1;
13944
+ }
13945
+ if (physics) physics._driveVehicle(this, dt);
13946
+ this.poseWheels();
13947
+ }
13948
+ /** `Wheel<i>` hangs on its spring and turns to steer; its `Hub` spins. */
13949
+ poseWheels() {
13950
+ const specs = this.specs();
13951
+ for (let i = 0; i < specs.length; i++) {
13952
+ const spec = specs[i];
13953
+ if (!spec) continue;
13954
+ const wheel = this.parent?.getNodeOrNull(`Wheel${i}`);
13955
+ if (!wheel || !(wheel instanceof Node3D)) continue;
13956
+ const s = this.wheel(i);
13957
+ wheel.position = [
13958
+ spec.position[0],
13959
+ spec.position[1] - s.suspension,
13960
+ spec.position[2]
13961
+ ];
13962
+ wheel.rotation = [
13963
+ 0,
13964
+ s.steering * RAD2DEG,
13965
+ 0
13966
+ ];
13967
+ const hub = wheel.getNodeOrNull("Hub");
13968
+ if (hub && hub instanceof Node3D) hub.rotation = [
13969
+ s.rotation * RAD2DEG,
13970
+ hub.rotation[1] ?? 0,
13971
+ hub.rotation[2] ?? 0
13972
+ ];
13973
+ }
13974
+ }
13975
+ /** Full lock, radians. */
13976
+ get maxSteer() {
13977
+ return this.maxSteerDeg * DEG;
13978
+ }
13979
+ };
13980
+ //#endregion
12247
13981
  //#region src/3d/nodes/voxel-grid-3d.ts
12248
13982
  /**
12249
13983
  * Face colors per tile (front,right,back,left,top,bottom) — the agent8
@@ -13282,6 +15016,213 @@ void main() {
13282
15016
  }
13283
15017
  `;
13284
15018
  //#endregion
15019
+ //#region src/3d/nodes/weather-3d.ts
15020
+ /**
15021
+ * Rain falls this fast (m/s); snow this slow. Sizes are in px like every
15022
+ * particle prop (×0.01 = world metres, attenuated with distance): a 3 px drop
15023
+ * is 3 cm, sub-pixel ten metres away and invisible against a bright sky —
15024
+ * measured on the rooftops — so a drop is a hand wide and a flake wider.
15025
+ */
15026
+ const RAIN = {
15027
+ speed: [10, 14],
15028
+ size: 55,
15029
+ color: "#eaf3ff",
15030
+ alpha: .75,
15031
+ rate: 1500,
15032
+ spread: 4,
15033
+ cap: 4e3
15034
+ };
15035
+ const SNOW = {
15036
+ speed: [.6, 1.4],
15037
+ size: 14,
15038
+ color: "#ffffff",
15039
+ alpha: .9,
15040
+ rate: 500,
15041
+ spread: 50,
15042
+ cap: 6e3
15043
+ };
15044
+ const M_TO_PX = 100;
15045
+ /** How often the tree is re-scanned for the current camera (fixed steps). */
15046
+ const CAMERA_SCAN_STEPS = 60;
15047
+ /**
15048
+ * Weather — rain or snow that follows the camera.
15049
+ *
15050
+ * The scene format had a sky, a fog, a cloud deck and a sun, and no weather:
15051
+ * rain was forty lines of `Particles3D` tuning an author had to know, plus a
15052
+ * script to keep the emitter over the camera. One node now:
15053
+ *
15054
+ * ```json
15055
+ * { "name": "Rain", "type": "Weather3D", "props": { "kind": "rain", "intensity": 0.7, "wind": [2, 0] } }
15056
+ * ```
15057
+ *
15058
+ * The drops are born anywhere in a box `radius` wide and `height` above
15059
+ * whatever is followed (the current `Camera3D` unless `follow` names a node),
15060
+ * fall in world space, and stay where they fell when the camera moves on.
15061
+ * Every `Particles3D` prop is still here to override (a colour, a size), but
15062
+ * `kind` sets them all and `intensity` scales the rate live.
15063
+ */
15064
+ /**
15065
+ * A raindrop's stamp: a vertical streak with soft ends, not a dot.
15066
+ *
15067
+ * Rain is drawn as point sprites, and a point sprite is SQUARE — so the streak
15068
+ * lives inside the texture: a thin bright column down the middle with
15069
+ * transparent sides. Measured before this existed: a downpour at intensity 1
15070
+ * put 2,600 round dots on screen and read as a dirty lens.
15071
+ */
15072
+ let streakStamp = null;
15073
+ function streakSprite() {
15074
+ if (streakStamp) return streakStamp;
15075
+ const size = 64;
15076
+ const data = new Uint8Array(size * size * 4);
15077
+ const halfW = size * .055;
15078
+ const centre = (size - 1) / 2;
15079
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
15080
+ const dx = Math.abs(x - centre) / halfW;
15081
+ const along = Math.abs(y - centre) / centre;
15082
+ const ends = Math.max(0, 1 - Math.max(0, along - .72) / .28);
15083
+ const across = Math.max(0, 1 - dx * dx);
15084
+ const i = (y * size + x) * 4;
15085
+ data[i] = 255;
15086
+ data[i + 1] = 255;
15087
+ data[i + 2] = 255;
15088
+ data[i + 3] = Math.round(Math.min(1, across * ends) * 255);
15089
+ }
15090
+ const texture = new DataTexture(data, size, size, RGBAFormat);
15091
+ texture.needsUpdate = true;
15092
+ streakStamp = texture;
15093
+ return texture;
15094
+ }
15095
+ var Weather3D = class extends Particles3D {
15096
+ static typeName = "Weather3D";
15097
+ static props = {
15098
+ /** `rain` — fast, straight, a shower; `snow` — slow, drifting flakes. */
15099
+ kind: {
15100
+ default: "rain",
15101
+ options: ["rain", "snow"]
15102
+ },
15103
+ /** How much of it, 0..1 (scales the rate live; 0 = a dry sky). */
15104
+ intensity: {
15105
+ default: .6,
15106
+ range: {
15107
+ min: 0,
15108
+ max: 1
15109
+ }
15110
+ },
15111
+ /** Half-width (m) of the box the drops are born in, around the camera. */
15112
+ radius: { default: 20 },
15113
+ /** How far above the camera they are born (m). */
15114
+ height: { default: 12 },
15115
+ /** A steady drift, m/s, on the ground plane `[x, z]`. */
15116
+ wind: { default: [0, 0] },
15117
+ /** What to follow; '' = the current Camera3D. */
15118
+ follow: {
15119
+ default: "",
15120
+ nodePath: true
15121
+ }
15122
+ };
15123
+ kind = "rain";
15124
+ intensity = .6;
15125
+ radius = 20;
15126
+ height = 12;
15127
+ wind = [0, 0];
15128
+ follow = "";
15129
+ /** Rain is a streak; snow keeps the soft flake the particle system draws. */
15130
+ sprite() {
15131
+ return this.kind === "snow" ? super.sprite() : streakSprite();
15132
+ }
15133
+ tunedFor = "";
15134
+ camera = null;
15135
+ cameraScanIn = 0;
15136
+ onEnterTree() {
15137
+ super.onEnterTree?.();
15138
+ this.tune();
15139
+ }
15140
+ /** Set the inherited particle props from `kind` — once, and again if it changes. */
15141
+ tune() {
15142
+ const t = this.kind === "snow" ? SNOW : RAIN;
15143
+ this.worldSpace = true;
15144
+ this.blend = "normal";
15145
+ this.directionDeg = 90;
15146
+ this.spreadDeg = t.spread;
15147
+ this.speed = [(t.speed[0] ?? 1) * M_TO_PX, (t.speed[1] ?? 1) * M_TO_PX];
15148
+ const fall = t.speed[0] ?? 1;
15149
+ this.lifetime = [(this.height + 2) / fall, (this.height + 6) / fall];
15150
+ this.gravity = [
15151
+ 0,
15152
+ 0,
15153
+ 0
15154
+ ];
15155
+ this.drag = 0;
15156
+ this.sizeStart = t.size;
15157
+ this.sizeEnd = t.size;
15158
+ this.colorStart = t.color;
15159
+ this.colorEnd = t.color;
15160
+ this.alphaStart = t.alpha;
15161
+ this.alphaEnd = t.alpha * .8;
15162
+ this.maxParticles = t.cap;
15163
+ this.emitBox = [
15164
+ this.radius,
15165
+ .5,
15166
+ this.radius
15167
+ ];
15168
+ this.drift = [
15169
+ (this.wind[0] ?? 0) * M_TO_PX,
15170
+ 0,
15171
+ (this.wind[1] ?? 0) * M_TO_PX
15172
+ ];
15173
+ this.rate = t.rate * this.intensity;
15174
+ this.tunedFor = this.tuning();
15175
+ this._rebuildSim();
15176
+ }
15177
+ tuning() {
15178
+ return `${this.kind}/${this.radius}/${this.height}/${this.wind[0] ?? 0}/${this.wind[1] ?? 0}`;
15179
+ }
15180
+ followed() {
15181
+ if (this.follow !== "") return this.getNodeOrNull(this.follow);
15182
+ if (this.cameraScanIn <= 0 || !this.camera || this.camera.tree === null) {
15183
+ this.camera = currentCamera(this.getRoot());
15184
+ this.cameraScanIn = CAMERA_SCAN_STEPS;
15185
+ }
15186
+ this.cameraScanIn -= 1;
15187
+ return this.camera;
15188
+ }
15189
+ update(dt) {
15190
+ if (this.tunedFor !== this.tuning()) this.tune();
15191
+ const t = this.kind === "snow" ? SNOW : RAIN;
15192
+ this.rate = t.rate * Math.max(0, Math.min(1, this.intensity));
15193
+ const target = this.followed();
15194
+ if (target) {
15195
+ const at = worldPosition(target);
15196
+ const parentAt = this.parent ? worldPosition(this.parent) : [
15197
+ 0,
15198
+ 0,
15199
+ 0
15200
+ ];
15201
+ this.position = [
15202
+ (at[0] ?? 0) - (parentAt[0] ?? 0),
15203
+ (at[1] ?? 0) + this.height - (parentAt[1] ?? 0),
15204
+ (at[2] ?? 0) - (parentAt[2] ?? 0)
15205
+ ];
15206
+ }
15207
+ super.update(dt);
15208
+ }
15209
+ };
15210
+ function currentCamera(root) {
15211
+ let first = null;
15212
+ const walk = (n) => {
15213
+ if (n instanceof Camera3D) {
15214
+ if (n.current) return n;
15215
+ first ??= n;
15216
+ }
15217
+ for (const c of n.children) {
15218
+ const hit = walk(c);
15219
+ if (hit) return hit;
15220
+ }
15221
+ return null;
15222
+ };
15223
+ return walk(root) ?? first;
15224
+ }
15225
+ //#endregion
13285
15226
  //#region src/3d/register.ts
13286
15227
  /**
13287
15228
  * Register the 3D node taxonomy (and the core nodes). Call once in your game
@@ -13291,6 +15232,10 @@ function registerNodes3D() {
13291
15232
  registerCoreNodes();
13292
15233
  registerNode(Node3D);
13293
15234
  registerNode(Particles3D);
15235
+ registerNode(Weather3D);
15236
+ registerNode(Flock3D);
15237
+ registerNode(Ragdoll3D);
15238
+ registerNode(Vehicle3D);
13294
15239
  registerNode(MeshInstance3D);
13295
15240
  registerNode(LoftMesh3D);
13296
15241
  registerNode(Sprite3D);
@@ -13353,4 +15298,4 @@ function resolveEnvironmentHdri(env) {
13353
15298
  return null;
13354
15299
  }
13355
15300
  //#endregion
13356
- export { Billboard3D as A, cameraRelative as B, Flowers3D as C, Camera3D as D, CharacterController3D as E, Joint3D as F, acquireOwnTexture as G, movementState as H, InstancedMesh3D as I, acquireTexture as K, MeshInstance3D as L, DEFAULT_TERRAIN_TEXTURE_BASE as M, TERRAIN_THEMES as N, BoneLookAt3D as O, terrainThemeLayers as P, buildMeshGeometry as R, DENSITY_PRESETS as S, FLOWER_VARIETIES as T, rigPose as U, keyboardIntensity as V, TextureCache3D as W, LoftMesh3D as _, Tree3D as a, SpotLight3D as b, buildRiverRings as c, riverCarveChannels as d, riverStepFor as f, ModelInstance3D as g, Particles3D as h, VoxelGrid3D as i, Terrain3D as j, BoneAttachment3D as k, findRiverCoverageGaps as l, traceDownhillPath as m, registerNodes3D as n, Trail3D as o, smoothCourse as p, VOXEL_PALETTE as r, River3D as s, resolveEnvironmentHdri as t, projectToRiver as u, DirectionalLight3D as v, resolveFlowerDensity as w, Foliage3D as x, OmniLight3D as y, QUARTER_PITCH as z };
15301
+ export { CharacterController3D as A, InstancedMesh3D as B, SpotLight3D as C, resolveFlowerDensity as D, Flowers3D as E, Terrain3D as F, keyboardIntensity as G, buildMeshGeometry as H, DEFAULT_TERRAIN_TEXTURE_BASE as I, TextureCache3D as J, movementState as K, TERRAIN_THEMES as L, BoneLookAt3D as M, BoneAttachment3D as N, FLOWER_VARIETIES as O, Billboard3D as P, terrainThemeLayers as R, OmniLight3D as S, DENSITY_PRESETS as T, QUARTER_PITCH as U, MeshInstance3D as V, cameraRelative as W, acquireTexture as X, acquireOwnTexture as Y, Ragdoll3D as _, VoxelGrid3D as a, LoftMesh3D as b, Trail3D as c, findRiverCoverageGaps as d, projectToRiver as f, traceDownhillPath as g, smoothCourse as h, VOXEL_PALETTE as i, Camera3D as j, Flock3D as k, River3D as l, riverStepFor as m, registerNodes3D as n, Vehicle3D as o, riverCarveChannels as p, rigPose as q, Weather3D as r, Tree3D as s, resolveEnvironmentHdri as t, buildRiverRings as u, Particles3D as v, Foliage3D as w, DirectionalLight3D as x, ModelInstance3D as y, Joint3D as z };