incanto 0.74.0 → 0.76.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.
- package/bin/incanto-multiplay.mjs +52 -4
- package/dist/2d.d.ts +83 -7
- package/dist/2d.js +4 -3
- package/dist/3d.d.ts +861 -167
- package/dist/3d.js +6 -6
- package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
- package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
- package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
- package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
- package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
- package/dist/editor.js +1600 -1529
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
- package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
- package/dist/gameplay.d.ts +573 -31
- package/dist/gameplay.js +3 -2
- package/dist/index.d.ts +150 -6
- package/dist/index.js +6 -6
- package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
- package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
- package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
- package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
- package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
- package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
- package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
- package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
- package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
- package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
- package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
- package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
- package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
- package/dist/test.d.ts +90 -12
- package/dist/test.js +2 -2
- package/dist/vite.js +34 -5
- package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
- package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
- package/editor/assets/index-Db74LZY-.js +11579 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +1639 -280
- package/skills/incanto-3d-character.md +383 -20
- package/skills/incanto-audio.md +24 -2
- package/skills/incanto-building-2d-games.md +21 -2
- package/skills/incanto-building-3d-games.md +38 -3
- package/skills/incanto-editor.md +29 -8
- package/skills/incanto-environment.md +257 -14
- package/skills/incanto-game-feel.md +60 -1
- package/skills/incanto-gameplay-behaviors.md +668 -20
- package/skills/incanto-hud.md +116 -0
- package/skills/incanto-multiplayer.md +141 -2
- package/skills/incanto-node-reference.md +398 -16
- package/skills/incanto-performance.md +32 -0
- package/skills/incanto-physics-and-input.md +241 -13
- package/skills/incanto-playtesting.md +2 -2
- package/skills/incanto-save-slots.md +61 -2
- package/skills/incanto-scene-json-authoring.md +13 -0
- package/skills/incanto-verifying-your-game.md +86 -3
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
- package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/molehill-2d/src/game.scene.json +129 -2
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +40 -26
- package/templates-app/platformer-2d/src/game.scene.json +144 -2
- package/templates-app/platformer-2d/verify.ts +18 -1
- package/templates-app/star-survivor/PROJECT/Status.md +7 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/behaviors.ts +9 -8
- package/templates-app/star-survivor/src/game.scene.json +142 -4
- package/templates-app/star-survivor/verify.ts +13 -0
- package/templates-app/tps-3d/PROJECT/Status.md +6 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +71 -10
- package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
- package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
- package/templates-app/village-quest-3d/src/village.scene.json +126 -2
- package/templates-app/village-quest-3d/verify.ts +52 -2
- package/editor/assets/index-CrUCQoaB.js +0 -11046
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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-
|
|
8
|
-
import {
|
|
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 buildSeaSpectrum, it as CharacterBody3D, mt as seaFftSupported, nt as rejectScale, ot as RigidBody3D, pt as SeaFft, rt as Area3D, st as StaticBody3D, tt as WaterCutout3D, vt as findHealth } from "./gameplay-Bfff_beb.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
|
-
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";
|
|
11
|
+
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, HalfFloatType, 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, WebGLCubeRenderTarget } from "three";
|
|
12
12
|
import { clone } from "three/addons/utils/SkeletonUtils.js";
|
|
13
13
|
//#region src/3d/textures.ts
|
|
14
14
|
/**
|
|
@@ -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 · `
|
|
756
|
-
*
|
|
757
|
-
*
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
|
|
1765
|
-
|
|
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$
|
|
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$
|
|
2726
|
-
eulerScratch$1.y * RAD2DEG$
|
|
2727
|
-
eulerScratch$1.z * RAD2DEG$
|
|
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
|
-
|
|
2796
|
-
|
|
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,8 +2972,47 @@ 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
|
];
|
|
2982
|
+
/**
|
|
2983
|
+
* What to play when the scene's `animations` map has no clip for a state.
|
|
2984
|
+
*
|
|
2985
|
+
* A missing entry used to leave whatever was playing BEFORE, which is never
|
|
2986
|
+
* what anyone meant: a character swimming across a bay in a frozen jump pose,
|
|
2987
|
+
* because `swim` was unmapped and the last state was `airborne`. Every state
|
|
2988
|
+
* added after the original five had this — `dash`, `climb`, `sneak`, `crouch`
|
|
2989
|
+
* — and no scene in the repo mapped any of them.
|
|
2990
|
+
*
|
|
2991
|
+
* Each chain ends at `idle`, so any map with one clip in it animates.
|
|
2992
|
+
*/
|
|
2993
|
+
const CLIP_FALLBACK = {
|
|
2994
|
+
idle: [],
|
|
2995
|
+
walk: ["idle"],
|
|
2996
|
+
run: ["walk", "idle"],
|
|
2997
|
+
fastRun: [
|
|
2998
|
+
"run",
|
|
2999
|
+
"walk",
|
|
3000
|
+
"idle"
|
|
3001
|
+
],
|
|
3002
|
+
airborne: ["idle"],
|
|
3003
|
+
swim: ["walk", "idle"],
|
|
3004
|
+
climb: ["walk", "idle"],
|
|
3005
|
+
crouch: ["idle"],
|
|
3006
|
+
sneak: ["walk", "idle"],
|
|
3007
|
+
dash: [
|
|
3008
|
+
"fastRun",
|
|
3009
|
+
"run",
|
|
3010
|
+
"walk",
|
|
3011
|
+
"idle"
|
|
3012
|
+
]
|
|
3013
|
+
};
|
|
3014
|
+
/** How much of a swimmer's head rides clear of the water (m) — see `swimDepth`. */
|
|
3015
|
+
const HEAD_ABOVE_WATER = .15;
|
|
2816
3016
|
/** Air control stands down this long after a wall kick, so the kick lands. */
|
|
2817
3017
|
const WALL_LOCKOUT_SECONDS = .18;
|
|
2818
3018
|
const ACC_DELTA_TIME = 8;
|
|
@@ -2820,13 +3020,27 @@ const DRAG_DAMPING = .15;
|
|
|
2820
3020
|
const SPRING_K = 1.2;
|
|
2821
3021
|
const SPRING_DAMPING = .08;
|
|
2822
3022
|
const FALLING_MAX_VEL = -20;
|
|
3023
|
+
/** The swim spring: how hard the water returns the body to its waterline (1/s²) and damps it (1/s). */
|
|
3024
|
+
const SWIM_K = 30;
|
|
3025
|
+
const SWIM_DAMPING = 8;
|
|
3026
|
+
/** Lift cap while swimming, in g — water pushes back; it does not launch. */
|
|
3027
|
+
const SWIM_MAX_G = 3;
|
|
3028
|
+
const STANDARD_G = 9.81;
|
|
3029
|
+
/** A probe hit closer than this began inside the collider it hit. */
|
|
3030
|
+
const INSIDE_HIT = 1e-4;
|
|
3031
|
+
/** How fast a swimmer pulls themselves up onto a ledge (m/s). */
|
|
3032
|
+
const CLIMB_SPEED = 2;
|
|
3033
|
+
/** After letting go of a ladder, this long before it can be grabbed again (s). */
|
|
3034
|
+
const LADDER_LOCKOUT = .35;
|
|
3035
|
+
/** How often the controller re-scans the tree for Water3D nodes (fixed steps). */
|
|
3036
|
+
const WATER_SCAN_STEPS = 60;
|
|
2823
3037
|
/** The schema's own default for an action prop — never a second copy of it. */
|
|
2824
3038
|
function defaultActionOf(prop) {
|
|
2825
3039
|
const schema = CharacterController3D.props[prop];
|
|
2826
3040
|
return typeof schema?.default === "string" ? schema.default : "";
|
|
2827
3041
|
}
|
|
2828
3042
|
const LOOK_SPEED = .002;
|
|
2829
|
-
const RAD2DEG = 180 / Math.PI;
|
|
3043
|
+
const RAD2DEG$3 = 180 / Math.PI;
|
|
2830
3044
|
/** Spring-arm camera collision: keep the camera this far in front of an obstacle. */
|
|
2831
3045
|
const CAM_COLLISION_MARGIN = .3;
|
|
2832
3046
|
/** Never pull the orbit camera closer to the eye than this (avoids clipping into the skin). */
|
|
@@ -2853,8 +3067,21 @@ const UP_VEC = new Vector3(0, 1, 0);
|
|
|
2853
3067
|
*/
|
|
2854
3068
|
var CharacterController3D = class extends Node3D {
|
|
2855
3069
|
static typeName = "CharacterController3D";
|
|
2856
|
-
static signals = [
|
|
3070
|
+
static signals = [
|
|
3071
|
+
"movementStateChanged",
|
|
3072
|
+
"wallJumped",
|
|
3073
|
+
"lockChanged",
|
|
3074
|
+
"dashed"
|
|
3075
|
+
];
|
|
2857
3076
|
static props = {
|
|
3077
|
+
/**
|
|
3078
|
+
* Off = the controller lets go: no input, no forces, no camera, no
|
|
3079
|
+
* animation — the body is an ordinary RigidBody3D until it is on again.
|
|
3080
|
+
* The switch a game flips when the player gets into a car, sits in a
|
|
3081
|
+
* cutscene or opens a menu; flip the body's `enabled`/`visible` beside it
|
|
3082
|
+
* to park the character out of sight.
|
|
3083
|
+
*/
|
|
3084
|
+
enabled: { default: true },
|
|
2858
3085
|
view: {
|
|
2859
3086
|
default: "free",
|
|
2860
3087
|
options: [
|
|
@@ -2889,12 +3116,115 @@ var CharacterController3D = class extends Node3D {
|
|
|
2889
3116
|
fallGravity: { default: 2.5 },
|
|
2890
3117
|
floatHeight: { default: .01 },
|
|
2891
3118
|
stepHeight: { default: .35 },
|
|
3119
|
+
slopeLimitDeg: {
|
|
3120
|
+
default: 55,
|
|
3121
|
+
range: {
|
|
3122
|
+
min: 0,
|
|
3123
|
+
max: 90
|
|
3124
|
+
}
|
|
3125
|
+
},
|
|
3126
|
+
/** Swimming pace (m/s) in water too deep to stand in; 0 = no swimming (the body sinks). */
|
|
3127
|
+
swimSpeed: { default: 1.6 },
|
|
3128
|
+
/** How far the body's origin rides BELOW the surface while swimming (m). */
|
|
3129
|
+
swimDepth: {
|
|
3130
|
+
default: null,
|
|
3131
|
+
kinds: ["null", "number"]
|
|
3132
|
+
},
|
|
3133
|
+
/** Held: swim DOWN at `swimSpeed`. Absent from the input map = no diving. */
|
|
3134
|
+
diveAction: { default: "dive" },
|
|
3135
|
+
/** Ladder pace (m/s): push UP into an Area3D in `ladderGroup` and the body climbs it; 0 = no ladders. */
|
|
3136
|
+
climbSpeed: { default: 1.5 },
|
|
3137
|
+
/** Any Area3D in this group is a ladder. */
|
|
3138
|
+
ladderGroup: { default: "ladder" },
|
|
3139
|
+
/** 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. */
|
|
3140
|
+
mantleHeight: {
|
|
3141
|
+
default: 0,
|
|
3142
|
+
range: { min: 0 }
|
|
3143
|
+
},
|
|
3144
|
+
/**
|
|
3145
|
+
* The capsule's height while the crouch key is held (m); 0 = no crouch.
|
|
3146
|
+
* The body sinks by half the difference so the feet stay put, a duct
|
|
3147
|
+
* lower than the standing capsule is crawled through, and a ceiling that
|
|
3148
|
+
* will not let you stand keeps you down until you are out from under it.
|
|
3149
|
+
*/
|
|
3150
|
+
crouchHeight: {
|
|
3151
|
+
default: 0,
|
|
3152
|
+
range: { min: 0 }
|
|
3153
|
+
},
|
|
3154
|
+
/** Pace while crouched, as a fraction of the standing pace. */
|
|
3155
|
+
crouchSpeedMultiplier: {
|
|
3156
|
+
default: .5,
|
|
3157
|
+
range: { min: 0 }
|
|
3158
|
+
},
|
|
3159
|
+
/** Held = crouch. Missing from the input map = the character never crouches. */
|
|
3160
|
+
crouchAction: { default: "crouch" },
|
|
3161
|
+
/**
|
|
3162
|
+
* A roll: this fast (m/s) for `dashSeconds`, where the stick points or —
|
|
3163
|
+
* with no stick — where the skin faces; 0 = no dash. The velocity is held
|
|
3164
|
+
* for the roll's frames (the controller brakes toward its own pace every
|
|
3165
|
+
* step, so a single impulse dies), `state` reads `dash`, and `dashed`
|
|
3166
|
+
* fires once per roll — hang the i-frames on it (`Health.protect`).
|
|
3167
|
+
*/
|
|
3168
|
+
dashSpeed: {
|
|
3169
|
+
default: 0,
|
|
3170
|
+
range: { min: 0 }
|
|
3171
|
+
},
|
|
3172
|
+
dashSeconds: {
|
|
3173
|
+
default: .25,
|
|
3174
|
+
range: { min: 0 }
|
|
3175
|
+
},
|
|
3176
|
+
/** Seconds after a roll ends before the next can start. */
|
|
3177
|
+
dashCooldown: {
|
|
3178
|
+
default: 0,
|
|
3179
|
+
range: { min: 0 }
|
|
3180
|
+
},
|
|
3181
|
+
/** Pressed = roll. Missing from the input map = the character never dashes. */
|
|
3182
|
+
dashAction: { default: "dash" },
|
|
3183
|
+
/**
|
|
3184
|
+
* With no stick, roll BACKWARD — away from where the skin faces — instead
|
|
3185
|
+
* of forward: the Souls-like backstep a lock-on game wants (a neutral
|
|
3186
|
+
* roll into the thing you are locked on is the wrong way).
|
|
3187
|
+
*/
|
|
3188
|
+
dashBackstep: { default: false },
|
|
2892
3189
|
mouseLook: { default: true },
|
|
2893
3190
|
zoomMin: { default: 0 },
|
|
2894
3191
|
zoomMax: { default: 0 },
|
|
2895
3192
|
pitchMin: { default: -1.3 },
|
|
2896
3193
|
pitchMax: { default: 1.5 },
|
|
3194
|
+
/**
|
|
3195
|
+
* Where the camera RESTS before anyone has looked around, in degrees.
|
|
3196
|
+
* Negative looks down, which is what a third-person game usually wants.
|
|
3197
|
+
*
|
|
3198
|
+
* `pitch` is a radian field the mouse mutates, so the alternative was a
|
|
3199
|
+
* behaviour whose only job is one line in `onReady`. On a phone it stops
|
|
3200
|
+
* being a nicety: a portrait screen is very tall, so a level camera spends
|
|
3201
|
+
* the top half on sky and the bottom on the ground at the player's feet,
|
|
3202
|
+
* and a player with no mouse cannot fix it before they start dragging.
|
|
3203
|
+
* Clamped by `pitchMin`/`pitchMax` like every other pitch. `0` is the old
|
|
3204
|
+
* behaviour.
|
|
3205
|
+
*/
|
|
3206
|
+
pitchStart: {
|
|
3207
|
+
default: 0,
|
|
3208
|
+
range: {
|
|
3209
|
+
min: -180,
|
|
3210
|
+
max: 180
|
|
3211
|
+
}
|
|
3212
|
+
},
|
|
2897
3213
|
cameraCollision: { default: true },
|
|
3214
|
+
/**
|
|
3215
|
+
* Who owns the scene's camera: `'drive'` (default) is this controller, as
|
|
3216
|
+
* it always has been — the rig behind the character, per `view`. `'none'`
|
|
3217
|
+
* leaves the camera exactly where the scene puts it, for the whole shelf of
|
|
3218
|
+
* games with a FIXED view: a board game, a bomb arena, an isometric puzzle,
|
|
3219
|
+
* a fixed-angle horror. Those games could not use this controller at all,
|
|
3220
|
+
* because it re-posed their camera every frame; now they pair it with an
|
|
3221
|
+
* authored `Camera3D` (and a `FollowCamera` behaviour when the board
|
|
3222
|
+
* scrolls).
|
|
3223
|
+
*/
|
|
3224
|
+
camera: {
|
|
3225
|
+
default: "drive",
|
|
3226
|
+
options: ["drive", "none"]
|
|
3227
|
+
},
|
|
2898
3228
|
platformCarry: { default: true },
|
|
2899
3229
|
turnSpeed: { default: 100 },
|
|
2900
3230
|
camLerp: { default: 25 },
|
|
@@ -2906,6 +3236,20 @@ var CharacterController3D = class extends Node3D {
|
|
|
2906
3236
|
nodePath: true
|
|
2907
3237
|
},
|
|
2908
3238
|
skinYawOffset: { default: 0 },
|
|
3239
|
+
/**
|
|
3240
|
+
* The button that LOCKS ON: the camera yaw follows the nearest live member
|
|
3241
|
+
* of `lockGroup` within `lockRange`, and the skin FACES it while the feet go
|
|
3242
|
+
* anywhere — a locked player circling a brute shows it a face, not a
|
|
3243
|
+
* shoulder, and a hitbox hung off the skin's yaw swings at it. Press again
|
|
3244
|
+
* to let go; the lock lets go by itself when the target dies, leaves the
|
|
3245
|
+
* tree, or gets 1.5 × `lockRange` away. '' = no lock-on.
|
|
3246
|
+
*/
|
|
3247
|
+
lockOnAction: { default: "" },
|
|
3248
|
+
lockGroup: { default: "enemy" },
|
|
3249
|
+
lockRange: {
|
|
3250
|
+
default: 12,
|
|
3251
|
+
range: { min: 0 }
|
|
3252
|
+
},
|
|
2909
3253
|
animations: {
|
|
2910
3254
|
default: {},
|
|
2911
3255
|
asset: {
|
|
@@ -2914,6 +3258,7 @@ var CharacterController3D = class extends Node3D {
|
|
|
2914
3258
|
}
|
|
2915
3259
|
}
|
|
2916
3260
|
};
|
|
3261
|
+
enabled = true;
|
|
2917
3262
|
view = "free";
|
|
2918
3263
|
camDistance = 4;
|
|
2919
3264
|
eyeHeight = .64;
|
|
@@ -2940,6 +3285,60 @@ var CharacterController3D = class extends Node3D {
|
|
|
2940
3285
|
fallGravity = 2.5;
|
|
2941
3286
|
floatHeight = .01;
|
|
2942
3287
|
/**
|
|
3288
|
+
* Swimming pace in water too deep to stand in (m/s; `sprint` still
|
|
3289
|
+
* multiplies). `0` turns swimming OFF and the body sinks to the bed, which
|
|
3290
|
+
* is what a drowning pit wants and what every scene got before this.
|
|
3291
|
+
*/
|
|
3292
|
+
swimSpeed = 1.6;
|
|
3293
|
+
/**
|
|
3294
|
+
* How far the body's ORIGIN rides below the surface while swimming (m), or
|
|
3295
|
+
* `null` (the default) to take it from the body: deep enough that the head
|
|
3296
|
+
* rides 15 cm clear and the rest is in the water, which is what treading
|
|
3297
|
+
* water looks like.
|
|
3298
|
+
*
|
|
3299
|
+
* The origin is the middle of the capsule, so a fixed number is only ever
|
|
3300
|
+
* right for one character size. The 0.35 m this used to default to left the
|
|
3301
|
+
* head of a standard 1.68 m rig 0.46 m clear of the surface — a person
|
|
3302
|
+
* floating like a boat, chest and shoulders in the air.
|
|
3303
|
+
*/
|
|
3304
|
+
swimDepth = null;
|
|
3305
|
+
/** Held: swim down at `swimSpeed`. Missing from the input map is fine. */
|
|
3306
|
+
diveAction = "dive";
|
|
3307
|
+
/**
|
|
3308
|
+
* Ladder pace (m/s). A ladder is any `Area3D` in `ladderGroup`: push UP into
|
|
3309
|
+
* it and the body climbs, gravity off, held to the rungs; at the top it
|
|
3310
|
+
* pulls itself onto the ledge, down steps off at the bottom, `jump` lets go.
|
|
3311
|
+
* `0` = no ladders (every scene before this).
|
|
3312
|
+
*/
|
|
3313
|
+
climbSpeed = 1.5;
|
|
3314
|
+
ladderGroup = "ladder";
|
|
3315
|
+
/**
|
|
3316
|
+
* The pull-up, for any jump. A jump that falls a hand short of a ledge is
|
|
3317
|
+
* the most common near-miss in a platformer, and every one handles it the
|
|
3318
|
+
* same way: catch the edge, pull up. On the way down from a jump, a ledge
|
|
3319
|
+
* ahead no higher than this above the capsule's bottom is taken — the same
|
|
3320
|
+
* climb that takes a swimmer out of the water and a climber off a ladder.
|
|
3321
|
+
* `0` (the default) = a jump near a wall feels exactly as it did.
|
|
3322
|
+
*/
|
|
3323
|
+
mantleHeight = 0;
|
|
3324
|
+
crouchHeight = 0;
|
|
3325
|
+
crouchSpeedMultiplier = .5;
|
|
3326
|
+
crouchAction = "crouch";
|
|
3327
|
+
dashSpeed = 0;
|
|
3328
|
+
dashSeconds = .25;
|
|
3329
|
+
dashCooldown = 0;
|
|
3330
|
+
dashAction = "dash";
|
|
3331
|
+
dashBackstep = false;
|
|
3332
|
+
dashLeft = 0;
|
|
3333
|
+
dashCooldownLeft = 0;
|
|
3334
|
+
dashDir = [0, -1];
|
|
3335
|
+
/** Read-only: shorter right now — by the key, or by a ceiling. */
|
|
3336
|
+
crouching = false;
|
|
3337
|
+
/** The capsule height to stand back up to. */
|
|
3338
|
+
standingHeight = 0;
|
|
3339
|
+
/** The skin's authored y offset, restored on standing. */
|
|
3340
|
+
skinRest = null;
|
|
3341
|
+
/**
|
|
2943
3342
|
* How high a step the PLAYER walks up without jumping (0 = off).
|
|
2944
3343
|
*
|
|
2945
3344
|
* The hover spring holds the capsule a fixed distance above whatever is
|
|
@@ -2958,13 +3357,29 @@ var CharacterController3D = class extends Node3D {
|
|
|
2958
3357
|
* 200 kg character does not move a seesaw a 70 kg crate tips.
|
|
2959
3358
|
*/
|
|
2960
3359
|
stepHeight = .35;
|
|
3360
|
+
/**
|
|
3361
|
+
* The steepest ground this character STANDS on, in degrees (0-90).
|
|
3362
|
+
*
|
|
3363
|
+
* At or under it the ground is ground: the character holds its place with no
|
|
3364
|
+
* slide at all, however long you leave it there. Over it the ground stops
|
|
3365
|
+
* counting as ground — no hover spring, no `grounded`, no jump reset — and
|
|
3366
|
+
* the body slips off the face, which is what a slope limit means in every
|
|
3367
|
+
* engine that has one.
|
|
3368
|
+
*
|
|
3369
|
+
* 55° is a person. A mountain goat is 80; something on wheels is 25. `90`
|
|
3370
|
+
* turns the limit off — which is what this controller did before it had one,
|
|
3371
|
+
* and why it slid down a 7° hillside.
|
|
3372
|
+
*/
|
|
3373
|
+
slopeLimitDeg = 55;
|
|
2961
3374
|
mouseLook = true;
|
|
2962
3375
|
zoomMin = 0;
|
|
2963
3376
|
zoomMax = 0;
|
|
2964
3377
|
pitchMin = -1.3;
|
|
2965
3378
|
pitchMax = 1.5;
|
|
3379
|
+
pitchStart = 0;
|
|
2966
3380
|
/** Spring-arm: pull the orbit camera in front of walls/ground instead of clipping. */
|
|
2967
3381
|
cameraCollision = true;
|
|
3382
|
+
camera = "drive";
|
|
2968
3383
|
/**
|
|
2969
3384
|
* Ride whatever you are standing on.
|
|
2970
3385
|
*
|
|
@@ -2989,19 +3404,59 @@ var CharacterController3D = class extends Node3D {
|
|
|
2989
3404
|
* would fling the player on every step onto one.
|
|
2990
3405
|
*/
|
|
2991
3406
|
groundCarry(ground, dt) {
|
|
3407
|
+
const body = this.parent;
|
|
3408
|
+
const physics = body?._physics3d;
|
|
3409
|
+
const me = body ? worldPosition(body) : [
|
|
3410
|
+
0,
|
|
3411
|
+
0,
|
|
3412
|
+
0
|
|
3413
|
+
];
|
|
2992
3414
|
if (!this.platformCarry || !this.grounded || !ground || dt <= 0) {
|
|
2993
3415
|
this.carriedBy = ground;
|
|
2994
|
-
if (ground)
|
|
3416
|
+
if (ground) {
|
|
3417
|
+
this.carriedFrom = groundXZ(ground);
|
|
3418
|
+
this.carriedRot = physics?.rotationOf(ground) ?? [
|
|
3419
|
+
0,
|
|
3420
|
+
0,
|
|
3421
|
+
0,
|
|
3422
|
+
1
|
|
3423
|
+
];
|
|
3424
|
+
this.carriedMe = [me[0] ?? 0, me[2] ?? 0];
|
|
3425
|
+
}
|
|
2995
3426
|
return [0, 0];
|
|
2996
3427
|
}
|
|
2997
3428
|
const at = groundXZ(ground);
|
|
3429
|
+
const rot = physics?.rotationOf(ground) ?? [
|
|
3430
|
+
0,
|
|
3431
|
+
0,
|
|
3432
|
+
0,
|
|
3433
|
+
1
|
|
3434
|
+
];
|
|
2998
3435
|
const same = this.carriedBy === ground;
|
|
2999
3436
|
const was = this.carriedFrom;
|
|
3437
|
+
const wasRot = this.carriedRot;
|
|
3438
|
+
const wasMe = this.carriedMe;
|
|
3000
3439
|
this.carriedBy = ground;
|
|
3001
3440
|
this.carriedFrom = at;
|
|
3441
|
+
this.carriedRot = rot;
|
|
3442
|
+
this.carriedMe = [me[0] ?? 0, me[2] ?? 0];
|
|
3002
3443
|
if (!same) return [0, 0];
|
|
3003
|
-
|
|
3004
|
-
|
|
3444
|
+
let vx = (at[0] - was[0]) / dt;
|
|
3445
|
+
let vz = (at[1] - was[1]) / dt;
|
|
3446
|
+
const ox = wasMe[0] - was[0];
|
|
3447
|
+
const oz = wasMe[1] - was[1];
|
|
3448
|
+
const [rx, , rz] = rotateXZByDelta(rot, wasRot, ox, oz);
|
|
3449
|
+
vx += (rx - ox) / dt;
|
|
3450
|
+
vz += (rz - oz) / dt;
|
|
3451
|
+
return [vx, vz];
|
|
3452
|
+
}
|
|
3453
|
+
carriedRot = [
|
|
3454
|
+
0,
|
|
3455
|
+
0,
|
|
3456
|
+
0,
|
|
3457
|
+
1
|
|
3458
|
+
];
|
|
3459
|
+
carriedMe = [0, 0];
|
|
3005
3460
|
turnSpeed = 100;
|
|
3006
3461
|
/** Camera smoothing rate (1-exp(-rate·dt)); side view uses 5. */
|
|
3007
3462
|
camLerp = 25;
|
|
@@ -3016,6 +3471,68 @@ var CharacterController3D = class extends Node3D {
|
|
|
3016
3471
|
* character facing away from the camera until it first moves).
|
|
3017
3472
|
*/
|
|
3018
3473
|
skinYawOffset = 0;
|
|
3474
|
+
lockOnAction = "";
|
|
3475
|
+
lockGroup = "enemy";
|
|
3476
|
+
lockRange = 12;
|
|
3477
|
+
/** What the character is locked onto, or null. */
|
|
3478
|
+
lockTarget = null;
|
|
3479
|
+
/** Lock onto `node`: the camera yaw and the skin follow it from now on. */
|
|
3480
|
+
lock(node) {
|
|
3481
|
+
if (node === this.lockTarget) return;
|
|
3482
|
+
this.lockTarget = node;
|
|
3483
|
+
this.emit("lockChanged", node);
|
|
3484
|
+
}
|
|
3485
|
+
/** Let go. */
|
|
3486
|
+
unlock() {
|
|
3487
|
+
if (!this.lockTarget) return;
|
|
3488
|
+
this.lockTarget = null;
|
|
3489
|
+
this.emit("lockChanged", null);
|
|
3490
|
+
}
|
|
3491
|
+
/** Lock the nearest live, visible member of `lockGroup` within `lockRange` — or let go if locked. */
|
|
3492
|
+
toggleLock() {
|
|
3493
|
+
if (this.lockTarget) {
|
|
3494
|
+
this.unlock();
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
const body = this.parent;
|
|
3498
|
+
if (!body || !hasPositionArray(body)) return;
|
|
3499
|
+
const me = worldPosition(body);
|
|
3500
|
+
let best = null;
|
|
3501
|
+
let bestD = this.lockRange;
|
|
3502
|
+
for (const n of this.tree?.getNodesInGroup(this.lockGroup) ?? []) {
|
|
3503
|
+
if (n === body || !hasPositionArray(n)) continue;
|
|
3504
|
+
if (n.visible === false) continue;
|
|
3505
|
+
if (findHealth(n)?.health.isDead) continue;
|
|
3506
|
+
const p = worldPosition(n);
|
|
3507
|
+
const d = Math.hypot((p[0] ?? 0) - (me[0] ?? 0), (p[2] ?? 0) - (me[2] ?? 0));
|
|
3508
|
+
if (d < bestD) {
|
|
3509
|
+
bestD = d;
|
|
3510
|
+
best = n;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
if (best) this.lock(best);
|
|
3514
|
+
}
|
|
3515
|
+
/** Player → target on the ground plane, unit; null when not locked (and lets go when the lock is stale). */
|
|
3516
|
+
lockDirection() {
|
|
3517
|
+
const t = this.lockTarget;
|
|
3518
|
+
if (!t) return null;
|
|
3519
|
+
const body = this.parent;
|
|
3520
|
+
if (!t.tree || !body || !hasPositionArray(body) || !hasPositionArray(t) || findHealth(t)?.health.isDead) {
|
|
3521
|
+
this.unlock();
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
const me = worldPosition(body);
|
|
3525
|
+
const p = worldPosition(t);
|
|
3526
|
+
const dx = (p[0] ?? 0) - (me[0] ?? 0);
|
|
3527
|
+
const dz = (p[2] ?? 0) - (me[2] ?? 0);
|
|
3528
|
+
const d = Math.hypot(dx, dz);
|
|
3529
|
+
if (d > this.lockRange * 1.5) {
|
|
3530
|
+
this.unlock();
|
|
3531
|
+
return null;
|
|
3532
|
+
}
|
|
3533
|
+
if (d < .05) return null;
|
|
3534
|
+
return [dx / d, dz / d];
|
|
3535
|
+
}
|
|
3019
3536
|
/**
|
|
3020
3537
|
* Movement-state → animation asset map, applied to the `skinPath` model
|
|
3021
3538
|
* directly — no CharacterAnimator behavior needed:
|
|
@@ -3047,9 +3564,33 @@ var CharacterController3D = class extends Node3D {
|
|
|
3047
3564
|
/** Camera yaw/pitch in radians (mouse look mutates these). */
|
|
3048
3565
|
yaw = 0;
|
|
3049
3566
|
pitch = 0;
|
|
3050
|
-
/** idle | walk | run | fastRun | airborne — drive animations from this. */
|
|
3567
|
+
/** idle | walk | run | fastRun | airborne | swim — drive animations from this. */
|
|
3051
3568
|
state = "idle";
|
|
3052
3569
|
grounded = false;
|
|
3570
|
+
/** Slope of the ground under the character, degrees — 0 when there is none. */
|
|
3571
|
+
groundSlopeDeg = 0;
|
|
3572
|
+
/** Its outward normal, or null. `[0, 1, 0]` is flat ground. */
|
|
3573
|
+
groundNormal = null;
|
|
3574
|
+
/** In water too deep to stand in — held at the surface by the swim spring. */
|
|
3575
|
+
swimming = false;
|
|
3576
|
+
/** The head is under the surface (diving, or a wave over it). */
|
|
3577
|
+
submerged = false;
|
|
3578
|
+
/** Pulling up onto a ledge out of the water (see the climb in fixedUpdate). */
|
|
3579
|
+
climbing = false;
|
|
3580
|
+
/** On a ladder — an `Area3D` in `ladderGroup` the body overlaps and pushed up into. */
|
|
3581
|
+
onLadder = false;
|
|
3582
|
+
ladders = [];
|
|
3583
|
+
ladderScanIn = 0;
|
|
3584
|
+
ladderLockout = 0;
|
|
3585
|
+
/** Every Water3D in the tree, re-scanned every {@link WATER_SCAN_STEPS}. */
|
|
3586
|
+
waters = [];
|
|
3587
|
+
waterScanIn = 0;
|
|
3588
|
+
/**
|
|
3589
|
+
* A hop out of the water is in progress: the spring may catch a ledge on
|
|
3590
|
+
* the way down (the mantle) — a jump from land never gets that, or the
|
|
3591
|
+
* spring would cushion every landing.
|
|
3592
|
+
*/
|
|
3593
|
+
swimHop = false;
|
|
3053
3594
|
/** @internal Feel timers — see the jump block for what each one buys. */
|
|
3054
3595
|
coyoteLeft = 0;
|
|
3055
3596
|
bufferLeft = 0;
|
|
@@ -3059,6 +3600,12 @@ var CharacterController3D = class extends Node3D {
|
|
|
3059
3600
|
jumping = false;
|
|
3060
3601
|
/** Seconds of air control still suppressed after a wall kick. */
|
|
3061
3602
|
wallLockout = 0;
|
|
3603
|
+
/** What the anti-slide has learned it must keep paying to hold a slope. */
|
|
3604
|
+
slideHold = 0;
|
|
3605
|
+
/** Where the body stood last step, for the anti-slide's position error. */
|
|
3606
|
+
slideFrom = null;
|
|
3607
|
+
/** Seconds since the player last asked this character to move. */
|
|
3608
|
+
stillFor = 0;
|
|
3062
3609
|
/** The outward normal of the wall being pressed into this step, if any. */
|
|
3063
3610
|
wallNormal = null;
|
|
3064
3611
|
get body() {
|
|
@@ -3068,6 +3615,10 @@ var CharacterController3D = class extends Node3D {
|
|
|
3068
3615
|
}
|
|
3069
3616
|
onEnterTree() {
|
|
3070
3617
|
this.body;
|
|
3618
|
+
if (this.pitchStart !== 0) {
|
|
3619
|
+
const want = this.pitchStart * Math.PI / 180;
|
|
3620
|
+
this.pitch = Math.min(Math.max(want, -this.pitchMax), -this.pitchMin);
|
|
3621
|
+
}
|
|
3071
3622
|
if (this.skinPath !== "") {
|
|
3072
3623
|
const skin = this.getNodeOrNull(this.skinPath);
|
|
3073
3624
|
if (skin) skin.rotation = [
|
|
@@ -3082,9 +3633,15 @@ var CharacterController3D = class extends Node3D {
|
|
|
3082
3633
|
const physics = body._physics3d;
|
|
3083
3634
|
const input = this.tree?.engine?.input;
|
|
3084
3635
|
if (!physics || !input) return;
|
|
3636
|
+
if (!this.enabled) {
|
|
3637
|
+
this.readAction("moveAction", () => input.getVector(this.moveAction), null, "");
|
|
3638
|
+
this.readAction("jumpAction", () => input.isPressed(this.jumpAction), false, "");
|
|
3639
|
+
this.readAction("sprintAction", () => input.isPressed(this.sprintAction), false, "");
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3085
3642
|
const radius = body.collider.radius ?? .32;
|
|
3086
3643
|
const halfHeight = (body.collider.height ?? 1) / 2;
|
|
3087
|
-
const pos = worldOf(body);
|
|
3644
|
+
const pos = worldOf$1(body);
|
|
3088
3645
|
const vel = physics.velocityOf(body);
|
|
3089
3646
|
const mass = physics.massOf(body);
|
|
3090
3647
|
const floatingDis = radius + this.floatHeight;
|
|
@@ -3093,6 +3650,8 @@ var CharacterController3D = class extends Node3D {
|
|
|
3093
3650
|
let toi = null;
|
|
3094
3651
|
/** What the probe landed on — the platform, when there is one. */
|
|
3095
3652
|
let ground = null;
|
|
3653
|
+
/** …and which way its surface faces, for the slope limit and the spring. */
|
|
3654
|
+
let normal = null;
|
|
3096
3655
|
for (const [ox, oz] of [
|
|
3097
3656
|
[0, 0],
|
|
3098
3657
|
[radius, 0],
|
|
@@ -3109,18 +3668,60 @@ var CharacterController3D = class extends Node3D {
|
|
|
3109
3668
|
-1,
|
|
3110
3669
|
0
|
|
3111
3670
|
], rayLen, body);
|
|
3112
|
-
if (hit) {
|
|
3671
|
+
if (hit && hit.distance > INSIDE_HIT) {
|
|
3113
3672
|
const d = hit.distance;
|
|
3114
3673
|
if (toi === null || d < toi) {
|
|
3115
3674
|
toi = d;
|
|
3116
3675
|
ground = hit.node;
|
|
3676
|
+
const n = hit.normal;
|
|
3677
|
+
normal = n ? [
|
|
3678
|
+
n[0] ?? 0,
|
|
3679
|
+
n[1] ?? 1,
|
|
3680
|
+
n[2] ?? 0
|
|
3681
|
+
] : null;
|
|
3117
3682
|
}
|
|
3118
3683
|
if (ox === 0 && oz === 0) break;
|
|
3119
3684
|
}
|
|
3120
3685
|
}
|
|
3121
|
-
|
|
3122
|
-
const
|
|
3123
|
-
|
|
3686
|
+
const upDot = normal ? Math.max(-1, Math.min(1, normal[1] ?? 1)) : 1;
|
|
3687
|
+
const slopeDeg = toi === null ? 0 : Math.acos(Math.max(upDot, 0)) * 180 / Math.PI;
|
|
3688
|
+
this.groundSlopeDeg = slopeDeg;
|
|
3689
|
+
this.groundNormal = toi === null ? null : normal ?? [
|
|
3690
|
+
0,
|
|
3691
|
+
1,
|
|
3692
|
+
0
|
|
3693
|
+
];
|
|
3694
|
+
const standable = slopeDeg <= this.slopeLimitDeg;
|
|
3695
|
+
/** The spring's target: `floatHeight` of clearance PERPENDICULAR to the ground. */
|
|
3696
|
+
const holdDis = floatingDis / Math.max(upDot, .35);
|
|
3697
|
+
this.grounded = toi !== null && toi < holdDis * 2 && standable;
|
|
3698
|
+
if (!this.grounded) {
|
|
3699
|
+
this.slideHold = 0;
|
|
3700
|
+
this.slideFrom = null;
|
|
3701
|
+
}
|
|
3702
|
+
const carry = this.groundCarry(standable ? ground : null, dt);
|
|
3703
|
+
const footed = toi !== null && toi < holdDis * 1.3 && standable && (vel[1] ?? 0) <= .5;
|
|
3704
|
+
const water = this.swimSpeed > 0 ? this.surfaceAt(pos[0], pos[2]) : null;
|
|
3705
|
+
const surface = water === null ? null : water[0];
|
|
3706
|
+
const swimDepth = this.swimDepth ?? Math.max(halfHeight + radius - HEAD_ABOVE_WATER, .05);
|
|
3707
|
+
const waterline = surface === null ? null : surface - swimDepth;
|
|
3708
|
+
const standY = toi === null ? null : originY - toi + floatingDis + halfHeight;
|
|
3709
|
+
const swimming = waterline !== null && surface !== null && surface > pos[1] - halfHeight && waterline !== null && (standY === null || waterline > standY) && !(this.jumping && (vel[1] ?? 0) > .5);
|
|
3710
|
+
this.swimming = swimming;
|
|
3711
|
+
this.submerged = surface !== null && surface > pos[1] + halfHeight + radius;
|
|
3712
|
+
if (swimming) {
|
|
3713
|
+
this.jumping = false;
|
|
3714
|
+
this.rising = false;
|
|
3715
|
+
this.jumpsUsed = 0;
|
|
3716
|
+
this.coyoteLeft = 0;
|
|
3717
|
+
this.swimHop = false;
|
|
3718
|
+
}
|
|
3719
|
+
const mantling = this.swimHop && (vel[1] ?? 0) <= .5;
|
|
3720
|
+
if (this.lockOnAction !== "") {
|
|
3721
|
+
if (this.readAction("lockOnAction", () => input.justPressed(this.lockOnAction), false, "Lock-on will not toggle.")) this.toggleLock();
|
|
3722
|
+
}
|
|
3723
|
+
const lockDir = this.lockDirection();
|
|
3724
|
+
if (lockDir) this.yaw = Math.atan2(-lockDir[0], -lockDir[1]);
|
|
3124
3725
|
let mx = 0;
|
|
3125
3726
|
let mz = 0;
|
|
3126
3727
|
let moving = false;
|
|
@@ -3139,12 +3740,57 @@ var CharacterController3D = class extends Node3D {
|
|
|
3139
3740
|
} catch (error) {
|
|
3140
3741
|
this.reportMissingAction("moveAction", error, "The character will not move.");
|
|
3141
3742
|
}
|
|
3743
|
+
const hitHealth = findHealth(this.parent)?.health ?? null;
|
|
3744
|
+
const staggered = hitHealth?.staggered ?? false;
|
|
3745
|
+
if (staggered) {
|
|
3746
|
+
mx = 0;
|
|
3747
|
+
mz = 0;
|
|
3748
|
+
moving = false;
|
|
3749
|
+
}
|
|
3750
|
+
const kick = hitHealth?.knocked ? hitHealth.kickVelocity : [];
|
|
3751
|
+
const lift = hitHealth?.knocked ? hitHealth.takeLift() : 0;
|
|
3752
|
+
this.dashCooldownLeft = Math.max(0, this.dashCooldownLeft - dt);
|
|
3753
|
+
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) {
|
|
3754
|
+
this.dashLeft = this.dashSeconds;
|
|
3755
|
+
this.dashCooldownLeft = this.dashSeconds + this.dashCooldown;
|
|
3756
|
+
if (moving) this.dashDir = [mx, mz];
|
|
3757
|
+
else {
|
|
3758
|
+
const skin = this.skinPath !== "" ? this.getNodeOrNull(this.skinPath) : null;
|
|
3759
|
+
if (skin) {
|
|
3760
|
+
const yaw = ((skin.rotation[1] ?? 0) - this.skinYawOffset) * (Math.PI / 180);
|
|
3761
|
+
const back = this.dashBackstep ? -1 : 1;
|
|
3762
|
+
this.dashDir = [Math.sin(yaw) * back, Math.cos(yaw) * back];
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
this.emit("dashed");
|
|
3766
|
+
}
|
|
3767
|
+
const dashing = this.dashLeft > 0;
|
|
3768
|
+
if (dashing) this.dashLeft -= dt;
|
|
3769
|
+
const rawUp = -this.readAction("moveAction", () => input.getVector(this.moveAction).y, 0, "The character will not climb.");
|
|
3142
3770
|
const sprinting = this.readAction("sprintAction", () => input.isPressed(this.sprintAction), false, "Sprint will never engage.");
|
|
3143
3771
|
const intensity = keyboardIntensity(moving, sprinting);
|
|
3144
3772
|
const speedMultiplier = 1 + (this.sprintMultiplier - 1) * intensity;
|
|
3145
|
-
const
|
|
3773
|
+
const walkMultiplier = 1 + (this.sprintMultiplier - 1) * keyboardIntensity(true, false);
|
|
3774
|
+
const targetSpeed = !moving ? 0 : swimming ? this.swimSpeed * speedMultiplier / walkMultiplier : this.maxSpeed * speedMultiplier * (this.crouching ? this.crouchSpeedMultiplier : 1);
|
|
3775
|
+
this.ladderLockout = Math.max(0, this.ladderLockout - dt);
|
|
3776
|
+
const ladder = this.climbSpeed > 0 && !swimming && this.ladderLockout <= 0 ? this.ladderUnder(body) : null;
|
|
3777
|
+
let onLadder = false;
|
|
3778
|
+
if (ladder) {
|
|
3779
|
+
const wantsUp = rawUp > .1;
|
|
3780
|
+
const wantsDown = rawUp < -.1;
|
|
3781
|
+
if (this.onLadder) onLadder = !(footed && wantsDown);
|
|
3782
|
+
else if (wantsUp || wantsDown && !footed) onLadder = true;
|
|
3783
|
+
}
|
|
3784
|
+
this.onLadder = onLadder;
|
|
3785
|
+
if (onLadder) {
|
|
3786
|
+
this.jumping = false;
|
|
3787
|
+
this.rising = false;
|
|
3788
|
+
this.jumpsUsed = 0;
|
|
3789
|
+
this.coyoteLeft = 0;
|
|
3790
|
+
this.swimHop = false;
|
|
3791
|
+
}
|
|
3146
3792
|
this.wallNormal = null;
|
|
3147
|
-
if ((this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0)) && !this.grounded && moving) {
|
|
3793
|
+
if ((this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0)) && !this.grounded && !swimming && !onLadder && moving) {
|
|
3148
3794
|
const r = body.collider.radius ?? .32;
|
|
3149
3795
|
const n = (this.tree?.engine?.physics?.castRay([
|
|
3150
3796
|
pos[0] ?? 0,
|
|
@@ -3165,7 +3811,42 @@ var CharacterController3D = class extends Node3D {
|
|
|
3165
3811
|
}
|
|
3166
3812
|
}
|
|
3167
3813
|
this.wallLockout = Math.max(0, this.wallLockout - dt);
|
|
3168
|
-
|
|
3814
|
+
let climbing = false;
|
|
3815
|
+
const mantleReach = this.mantleHeight > 0 && this.jumping && (vel[1] ?? 0) <= .5;
|
|
3816
|
+
const reach = mantleReach ? Math.max(this.mantleHeight, this.stepHeight) : this.stepHeight;
|
|
3817
|
+
if (reach > 0 && moving && (swimming || mantling || onLadder || mantleReach || this.climbing)) {
|
|
3818
|
+
const ahead = radius + .12;
|
|
3819
|
+
const stepFrom = water === null ? originY : water[1];
|
|
3820
|
+
const probeTop = stepFrom + reach + .2;
|
|
3821
|
+
const ledge = physics.castRay([
|
|
3822
|
+
pos[0] + mx * ahead,
|
|
3823
|
+
probeTop,
|
|
3824
|
+
pos[2] + mz * ahead
|
|
3825
|
+
], [
|
|
3826
|
+
0,
|
|
3827
|
+
-1,
|
|
3828
|
+
0
|
|
3829
|
+
], reach + .2 + rayLen, body);
|
|
3830
|
+
if (ledge && ledge.distance > INSIDE_HIT) {
|
|
3831
|
+
const top = probeTop - ledge.distance;
|
|
3832
|
+
const aboveTheBottom = top > originY - radius + .02;
|
|
3833
|
+
const withinReach = top - stepFrom <= reach + .02;
|
|
3834
|
+
const stillBelow = originY < top + floatingDis - .01;
|
|
3835
|
+
if (aboveTheBottom && withinReach && stillBelow) climbing = true;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
this.climbing = climbing;
|
|
3839
|
+
if (climbing && (vel[1] ?? 0) < CLIMB_SPEED) body.applyImpulse([
|
|
3840
|
+
0,
|
|
3841
|
+
(CLIMB_SPEED - (vel[1] ?? 0)) * .5 * mass,
|
|
3842
|
+
0
|
|
3843
|
+
]);
|
|
3844
|
+
if (onLadder && !climbing) body.applyImpulse([
|
|
3845
|
+
-(vel[0] ?? 0) * .3 * mass,
|
|
3846
|
+
(rawUp * this.climbSpeed - (vel[1] ?? 0)) * .5 * mass,
|
|
3847
|
+
-(vel[2] ?? 0) * .3 * mass
|
|
3848
|
+
]);
|
|
3849
|
+
const control = this.wallLockout > 0 || onLadder && !climbing ? 0 : this.grounded || swimming || climbing ? 1 : this.airControl;
|
|
3169
3850
|
const ax = (mx * targetSpeed + carry[0] - vel[0]) / ACC_DELTA_TIME * control;
|
|
3170
3851
|
const az = (mz * targetSpeed + carry[1] - vel[2]) / ACC_DELTA_TIME * control;
|
|
3171
3852
|
body.applyImpulse([
|
|
@@ -3173,13 +3854,40 @@ var CharacterController3D = class extends Node3D {
|
|
|
3173
3854
|
0,
|
|
3174
3855
|
az * mass
|
|
3175
3856
|
]);
|
|
3176
|
-
if (!moving && this.grounded) body.applyImpulse([
|
|
3857
|
+
if (!moving && (this.grounded || swimming)) body.applyImpulse([
|
|
3177
3858
|
-(vel[0] - carry[0]) * DRAG_DAMPING * mass,
|
|
3178
3859
|
0,
|
|
3179
3860
|
-(vel[2] - carry[1]) * DRAG_DAMPING * mass
|
|
3180
3861
|
]);
|
|
3862
|
+
this.stillFor = moving ? 0 : this.stillFor + dt;
|
|
3863
|
+
if (this.grounded && !swimming && !climbing && !onLadder && !this.jumping) {
|
|
3864
|
+
const n = this.groundNormal;
|
|
3865
|
+
const nx = n?.[0] ?? 0;
|
|
3866
|
+
const nz = n?.[2] ?? 0;
|
|
3867
|
+
const sinSlope = Math.hypot(nx, nz);
|
|
3868
|
+
if (sinSlope > .001) {
|
|
3869
|
+
const ux = nx / sinSlope;
|
|
3870
|
+
const uz = nz / sinSlope;
|
|
3871
|
+
const drift = (vel[0] - carry[0]) * ux + (vel[2] - carry[1]) * uz;
|
|
3872
|
+
if (this.stillFor < .1) {
|
|
3873
|
+
this.slideHold = 0;
|
|
3874
|
+
this.slideFrom = null;
|
|
3875
|
+
} else {
|
|
3876
|
+
const from = this.slideFrom;
|
|
3877
|
+
const moved = from === null ? 0 : (pos[0] - from[0]) * ux + (pos[2] - from[1]) * uz;
|
|
3878
|
+
this.slideFrom = [pos[0], pos[2]];
|
|
3879
|
+
this.slideHold = Math.max(-2, Math.min(2, this.slideHold + moved / dt * .5));
|
|
3880
|
+
const hold = drift + this.slideHold;
|
|
3881
|
+
if (hold !== 0) body.applyImpulse([
|
|
3882
|
+
-hold * ux * mass,
|
|
3883
|
+
0,
|
|
3884
|
+
-hold * uz * mass
|
|
3885
|
+
]);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3181
3889
|
let springToi = toi;
|
|
3182
|
-
if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && (vel[1] ?? 0) <= .5) {
|
|
3890
|
+
if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && !swimming && !climbing && (vel[1] ?? 0) <= .5) {
|
|
3183
3891
|
const ahead = radius + .12;
|
|
3184
3892
|
const probeTop = originY + this.stepHeight + .05;
|
|
3185
3893
|
const stepHit = physics.castRay([
|
|
@@ -3191,34 +3899,107 @@ var CharacterController3D = class extends Node3D {
|
|
|
3191
3899
|
-1,
|
|
3192
3900
|
0
|
|
3193
3901
|
], this.stepHeight + .05 + rayLen, body);
|
|
3194
|
-
if (stepHit) {
|
|
3902
|
+
if (stepHit && stepHit.distance > INSIDE_HIT) {
|
|
3195
3903
|
const rise = probeTop - stepHit.distance - (originY - toi);
|
|
3196
3904
|
if (rise > .02 && rise <= this.stepHeight + .02) springToi = toi - rise;
|
|
3197
3905
|
}
|
|
3198
3906
|
}
|
|
3199
|
-
if (springToi !== null && springToi < floatingDis * 2 && !this.jumping && (vel[1] ?? 0) < 2) {
|
|
3200
|
-
const spring = SPRING_K * (
|
|
3907
|
+
if (springToi !== null && springToi < floatingDis * 2 && (!this.jumping || mantling) && !swimming && !climbing && !onLadder && (vel[1] ?? 0) < 2) {
|
|
3908
|
+
const spring = SPRING_K * (holdDis - springToi) - vel[1] * SPRING_DAMPING;
|
|
3201
3909
|
body.applyImpulse([
|
|
3202
3910
|
0,
|
|
3203
3911
|
spring * mass,
|
|
3204
3912
|
0
|
|
3205
3913
|
]);
|
|
3206
3914
|
}
|
|
3915
|
+
const diving = this.swimSpeed > 0 && this.readAction("diveAction", () => input.isPressed(this.diveAction), false, "The character will never dive.");
|
|
3916
|
+
if (swimming && !climbing && waterline !== null) {
|
|
3917
|
+
const surfacing = this.submerged && this.readAction("jumpAction", () => input.isPressed(this.jumpAction), false, "The character will never surface by choice.");
|
|
3918
|
+
const vy = vel[1] ?? 0;
|
|
3919
|
+
let ay;
|
|
3920
|
+
if (diving) ay = (-this.swimSpeed - vy) / ACC_DELTA_TIME / dt;
|
|
3921
|
+
else if (surfacing) ay = (this.swimSpeed - vy) / ACC_DELTA_TIME / dt;
|
|
3922
|
+
else ay = SWIM_K * (waterline - pos[1]) - SWIM_DAMPING * vy;
|
|
3923
|
+
ay = Math.max(-3 * STANDARD_G, Math.min(SWIM_MAX_G * STANDARD_G, ay));
|
|
3924
|
+
body.applyImpulse([
|
|
3925
|
+
0,
|
|
3926
|
+
ay * dt * mass,
|
|
3927
|
+
0
|
|
3928
|
+
]);
|
|
3929
|
+
}
|
|
3207
3930
|
if (footed) {
|
|
3208
3931
|
this.coyoteLeft = this.coyoteSeconds;
|
|
3209
3932
|
this.jumpsUsed = 0;
|
|
3210
3933
|
this.rising = false;
|
|
3211
3934
|
this.jumping = false;
|
|
3935
|
+
this.swimHop = false;
|
|
3212
3936
|
} else {
|
|
3213
3937
|
this.coyoteLeft = Math.max(0, this.coyoteLeft - dt);
|
|
3214
|
-
if (!this.grounded && this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
|
|
3938
|
+
if (!this.grounded && !swimming && this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
|
|
3215
3939
|
}
|
|
3216
3940
|
const pressedJump = this.readAction("jumpAction", () => input.justPressed(this.jumpAction), false, "The character will never jump.");
|
|
3217
3941
|
if (pressedJump) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
|
|
3218
3942
|
else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
|
|
3219
|
-
|
|
3943
|
+
let wantsJump = this.bufferLeft > 0 || pressedJump;
|
|
3944
|
+
if (staggered) wantsJump = false;
|
|
3945
|
+
const wantCrouch = this.crouchHeight > 0 && this.readAction("crouchAction", () => input.isPressed(this.crouchAction), false, "The character will never crouch.");
|
|
3946
|
+
if (!this.crouching) {
|
|
3947
|
+
const standing = halfHeight * 2;
|
|
3948
|
+
if (wantCrouch && this.grounded && this.crouchHeight < standing) {
|
|
3949
|
+
this.standingHeight = standing;
|
|
3950
|
+
this.crouching = true;
|
|
3951
|
+
this.setHeight(body, this.crouchHeight, standing);
|
|
3952
|
+
}
|
|
3953
|
+
} else {
|
|
3954
|
+
if (!wantCrouch) {
|
|
3955
|
+
const rise = this.standingHeight - this.crouchHeight;
|
|
3956
|
+
const top = pos[1] + this.crouchHeight / 2 + radius - .02;
|
|
3957
|
+
if (!physics.castRay([
|
|
3958
|
+
pos[0],
|
|
3959
|
+
top,
|
|
3960
|
+
pos[2]
|
|
3961
|
+
], [
|
|
3962
|
+
0,
|
|
3963
|
+
1,
|
|
3964
|
+
0
|
|
3965
|
+
], rise + .06, body)) {
|
|
3966
|
+
this.crouching = false;
|
|
3967
|
+
this.setHeight(body, this.standingHeight, this.crouchHeight);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
if (this.crouching) {
|
|
3971
|
+
wantsJump = false;
|
|
3972
|
+
this.bufferLeft = 0;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3220
3975
|
const wall = this.wallNormal;
|
|
3221
|
-
if (wantsJump &&
|
|
3976
|
+
if (wantsJump && onLadder && ladder) {
|
|
3977
|
+
const at = worldOf$1(ladder);
|
|
3978
|
+
let ax = pos[0] - at[0];
|
|
3979
|
+
let az = pos[2] - at[2];
|
|
3980
|
+
const len = Math.hypot(ax, az);
|
|
3981
|
+
if (len > .001) {
|
|
3982
|
+
ax /= len;
|
|
3983
|
+
az /= len;
|
|
3984
|
+
} else {
|
|
3985
|
+
ax = -mx;
|
|
3986
|
+
az = -mz;
|
|
3987
|
+
}
|
|
3988
|
+
body.linearVelocity = [
|
|
3989
|
+
ax * 2,
|
|
3990
|
+
2.5,
|
|
3991
|
+
az * 2
|
|
3992
|
+
];
|
|
3993
|
+
vel[0] = ax * 2;
|
|
3994
|
+
vel[1] = 2.5;
|
|
3995
|
+
vel[2] = az * 2;
|
|
3996
|
+
this.onLadder = false;
|
|
3997
|
+
this.ladderLockout = LADDER_LOCKOUT;
|
|
3998
|
+
this.bufferLeft = 0;
|
|
3999
|
+
this.rising = false;
|
|
4000
|
+
this.jumping = true;
|
|
4001
|
+
this.jumpsUsed = this.maxJumps;
|
|
4002
|
+
} else if (wantsJump && !this.grounded && wall && this.wallJumpImpulse.some((v) => v !== 0)) {
|
|
3222
4003
|
const away = this.wallJumpImpulse[0] ?? 0;
|
|
3223
4004
|
const up = this.wallJumpImpulse[1] ?? 0;
|
|
3224
4005
|
body.linearVelocity = [
|
|
@@ -3236,8 +4017,10 @@ var CharacterController3D = class extends Node3D {
|
|
|
3236
4017
|
this.jumping = true;
|
|
3237
4018
|
this.wallLockout = WALL_LOCKOUT_SECONDS;
|
|
3238
4019
|
this.emit("wallJumped", wall);
|
|
3239
|
-
} else if (wantsJump && (footed || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
|
|
3240
|
-
const
|
|
4020
|
+
} else if (wantsJump && !onLadder && !(swimming && this.submerged) && (footed || swimming || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
|
|
4021
|
+
const hopVel = swimming ? Math.max(this.jumpVelocity, Math.sqrt(2 * this.sceneGravity() * (this.effectiveSwimDepth() + halfHeight + this.stepHeight + .05))) : 0;
|
|
4022
|
+
const jumpVel = swimming ? hopVel : this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
|
|
4023
|
+
if (swimming) this.swimHop = true;
|
|
3241
4024
|
body.linearVelocity = [
|
|
3242
4025
|
vel[0],
|
|
3243
4026
|
jumpVel,
|
|
@@ -3265,6 +4048,7 @@ var CharacterController3D = class extends Node3D {
|
|
|
3265
4048
|
}
|
|
3266
4049
|
}
|
|
3267
4050
|
const sliding = this.wallSlideSpeed > 0 && !footed && this.wallNormal !== null && (vel[1] ?? 0) < 0;
|
|
4051
|
+
const atSlideCap = sliding && (vel[1] ?? 0) <= -this.wallSlideSpeed + .01;
|
|
3268
4052
|
if (sliding && (vel[1] ?? 0) < -this.wallSlideSpeed) {
|
|
3269
4053
|
body.linearVelocity = [
|
|
3270
4054
|
vel[0],
|
|
@@ -3273,7 +4057,8 @@ var CharacterController3D = class extends Node3D {
|
|
|
3273
4057
|
];
|
|
3274
4058
|
vel[1] = -this.wallSlideSpeed;
|
|
3275
4059
|
}
|
|
3276
|
-
if (
|
|
4060
|
+
if (swimming || climbing || atSlideCap || this.onLadder) body.gravityScale = 0;
|
|
4061
|
+
else if ((vel[1] ?? 0) < FALLING_MAX_VEL) body.gravityScale = 0;
|
|
3277
4062
|
else if ((vel[1] ?? 0) < 0 && !footed) body.gravityScale = sliding ? 1 : this.fallGravity;
|
|
3278
4063
|
else body.gravityScale = 1;
|
|
3279
4064
|
if (this.view === "side" && Math.abs(pos[2]) > .01) body.position = [
|
|
@@ -3281,23 +4066,46 @@ var CharacterController3D = class extends Node3D {
|
|
|
3281
4066
|
body.position[1] ?? 0,
|
|
3282
4067
|
0
|
|
3283
4068
|
];
|
|
3284
|
-
|
|
4069
|
+
if (dashing && !swimming) body.linearVelocity = [
|
|
4070
|
+
this.dashDir[0] * this.dashSpeed,
|
|
4071
|
+
vel[1] ?? 0,
|
|
4072
|
+
this.dashDir[1] * this.dashSpeed
|
|
4073
|
+
];
|
|
4074
|
+
if (kick.length > 0) {
|
|
4075
|
+
const cur = body.linearVelocity;
|
|
4076
|
+
body.linearVelocity = [
|
|
4077
|
+
kick[0] ?? 0,
|
|
4078
|
+
lift !== 0 ? lift : cur[1] ?? 0,
|
|
4079
|
+
kick[2] ?? 0
|
|
4080
|
+
];
|
|
4081
|
+
}
|
|
4082
|
+
const airborne = this.jumping || !this.grounded;
|
|
4083
|
+
const next = dashing && !swimming ? "dash" : swimming ? "swim" : this.onLadder || climbing ? "climb" : this.crouching && !airborne ? moving ? "sneak" : "crouch" : movementState(!airborne, moving ? intensity : 0);
|
|
3285
4084
|
if (next !== this.state) {
|
|
3286
4085
|
this.state = next;
|
|
3287
|
-
|
|
4086
|
+
let clip = this.animations[next];
|
|
4087
|
+
if (!clip) for (const alt of CLIP_FALLBACK[next]) {
|
|
4088
|
+
const c = this.animations[alt];
|
|
4089
|
+
if (c) {
|
|
4090
|
+
clip = c;
|
|
4091
|
+
break;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
3288
4094
|
if (clip && this.skinPath !== "") {
|
|
3289
4095
|
const skin = this.getNodeOrNull(this.skinPath);
|
|
3290
4096
|
if (skin && "animation" in skin) skin.animation = clip;
|
|
3291
4097
|
}
|
|
3292
4098
|
this.emit("movementStateChanged", next);
|
|
3293
4099
|
}
|
|
3294
|
-
if (moving && this.skinPath !== "") {
|
|
4100
|
+
if ((moving || lockDir) && this.skinPath !== "") {
|
|
3295
4101
|
const skin = this.getNodeOrNull(this.skinPath);
|
|
3296
4102
|
if (skin) {
|
|
3297
|
-
const
|
|
4103
|
+
const fx = lockDir ? lockDir[0] : mx;
|
|
4104
|
+
const fz = lockDir ? lockDir[1] : mz;
|
|
4105
|
+
const target = Math.atan2(fx, fz) * RAD2DEG$3 + this.skinYawOffset;
|
|
3298
4106
|
const current = skin.rotation[1] ?? 0;
|
|
3299
4107
|
let delta = (target - current + 540) % 360 - 180;
|
|
3300
|
-
const step = this.turnSpeed * dt * RAD2DEG;
|
|
4108
|
+
const step = this.turnSpeed * dt * RAD2DEG$3;
|
|
3301
4109
|
if (Math.abs(delta) > step) delta = Math.sign(delta) * step;
|
|
3302
4110
|
skin.rotation = [
|
|
3303
4111
|
skin.rotation[0] ?? 0,
|
|
@@ -3310,6 +4118,84 @@ var CharacterController3D = class extends Node3D {
|
|
|
3310
4118
|
/** Actions already reported missing, so a per-frame read says it once. */
|
|
3311
4119
|
reportedActions = /* @__PURE__ */ new Set();
|
|
3312
4120
|
/**
|
|
4121
|
+
* The water over a world point — `[wave surface, still level]` — or null
|
|
4122
|
+
* when no `Water3D`'s footprint covers it. The tree is re-scanned once a
|
|
4123
|
+
* second, so a lake streamed in later is found; the wave height is the CPU
|
|
4124
|
+
* twin of what is drawn, so a swimmer rides the swell the way a raft does.
|
|
4125
|
+
*/
|
|
4126
|
+
/** The ladder the body is overlapping, if any — an Area3D in `ladderGroup`. */
|
|
4127
|
+
ladderUnder(body) {
|
|
4128
|
+
const physics = body._physics3d;
|
|
4129
|
+
if (!physics) return null;
|
|
4130
|
+
if (this.ladderScanIn <= 0) {
|
|
4131
|
+
this.ladders.length = 0;
|
|
4132
|
+
collectAreas(this.getRoot(), this.ladderGroup, this.ladders);
|
|
4133
|
+
this.ladderScanIn = WATER_SCAN_STEPS;
|
|
4134
|
+
}
|
|
4135
|
+
this.ladderScanIn -= 1;
|
|
4136
|
+
for (const ladder of this.ladders) {
|
|
4137
|
+
if (ladder.tree === null) continue;
|
|
4138
|
+
if (physics.overlapping(ladder).includes(body)) return ladder;
|
|
4139
|
+
}
|
|
4140
|
+
return null;
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Swap the capsule's height and keep the feet where they are: the body
|
|
4144
|
+
* drops (or rises) by half the difference in the same step, the skin's
|
|
4145
|
+
* authored offset moves the other way so the model's feet stay on the
|
|
4146
|
+
* floor, and the adapter rebuilds the body at the new place next step.
|
|
4147
|
+
*/
|
|
4148
|
+
setHeight(body, next, from) {
|
|
4149
|
+
body.collider = {
|
|
4150
|
+
...body.collider,
|
|
4151
|
+
height: next
|
|
4152
|
+
};
|
|
4153
|
+
const d = (from - next) / 2;
|
|
4154
|
+
const p = body.position;
|
|
4155
|
+
body.position = [
|
|
4156
|
+
p[0] ?? 0,
|
|
4157
|
+
(p[1] ?? 0) - d,
|
|
4158
|
+
p[2] ?? 0
|
|
4159
|
+
];
|
|
4160
|
+
if (this.skinPath === "") return;
|
|
4161
|
+
const skin = this.getNodeOrNull(this.skinPath);
|
|
4162
|
+
if (!skin) return;
|
|
4163
|
+
if (this.skinRest === null) this.skinRest = skin.position[1] ?? 0;
|
|
4164
|
+
const lift = this.crouching ? (this.standingHeight - this.crouchHeight) / 2 : 0;
|
|
4165
|
+
skin.position = [
|
|
4166
|
+
skin.position[0] ?? 0,
|
|
4167
|
+
this.skinRest + lift,
|
|
4168
|
+
skin.position[2] ?? 0
|
|
4169
|
+
];
|
|
4170
|
+
}
|
|
4171
|
+
sceneGravity() {
|
|
4172
|
+
const g = this.tree?.engine?.scene?.physics?.gravity;
|
|
4173
|
+
return Math.abs(g?.[1] ?? STANDARD_G) || STANDARD_G;
|
|
4174
|
+
}
|
|
4175
|
+
/** `swimDepth`, or what it is derived to be for this body (see the prop). */
|
|
4176
|
+
effectiveSwimDepth() {
|
|
4177
|
+
if (this.swimDepth !== null) return this.swimDepth;
|
|
4178
|
+
const body = this.parent;
|
|
4179
|
+
const radius = body?.collider?.radius ?? .32;
|
|
4180
|
+
const halfHeight = (body?.collider?.height ?? 1) / 2;
|
|
4181
|
+
return Math.max(halfHeight + radius - HEAD_ABOVE_WATER, .05);
|
|
4182
|
+
}
|
|
4183
|
+
surfaceAt(x, z) {
|
|
4184
|
+
if (this.waterScanIn <= 0) {
|
|
4185
|
+
this.waters.length = 0;
|
|
4186
|
+
collectWaters(this.getRoot(), this.waters);
|
|
4187
|
+
this.waterScanIn = WATER_SCAN_STEPS;
|
|
4188
|
+
}
|
|
4189
|
+
this.waterScanIn -= 1;
|
|
4190
|
+
for (const water of this.waters) {
|
|
4191
|
+
if (water.tree === null) continue;
|
|
4192
|
+
const still = water._waterYAt(x, z);
|
|
4193
|
+
if (still === null) continue;
|
|
4194
|
+
return [water.heightAt(x, z), still];
|
|
4195
|
+
}
|
|
4196
|
+
return null;
|
|
4197
|
+
}
|
|
4198
|
+
/**
|
|
3313
4199
|
* Read an input action this controller is allowed to be missing.
|
|
3314
4200
|
*
|
|
3315
4201
|
* Tolerated because a game may declare no sprint key and no jump; reported
|
|
@@ -3333,6 +4219,7 @@ var CharacterController3D = class extends Node3D {
|
|
|
3333
4219
|
}, void 0, consequence);
|
|
3334
4220
|
}
|
|
3335
4221
|
update(dt) {
|
|
4222
|
+
if (!this.enabled) return;
|
|
3336
4223
|
const input = this.tree?.engine?.input;
|
|
3337
4224
|
if (!input) return;
|
|
3338
4225
|
if (this.mouseLook) {
|
|
@@ -3346,11 +4233,12 @@ var CharacterController3D = class extends Node3D {
|
|
|
3346
4233
|
const wheel = input.wheelDelta();
|
|
3347
4234
|
if (wheel !== 0 && this.zoomMax > this.zoomMin) this.camDistance = clamp(this.camDistance + wheel * LOOK_SPEED, this.zoomMin, this.zoomMax);
|
|
3348
4235
|
}
|
|
4236
|
+
if (this.camera === "none") return;
|
|
3349
4237
|
const camera = this.findCamera();
|
|
3350
4238
|
if (!camera) return;
|
|
3351
4239
|
const body = this.parent instanceof RigidBody3D ? this.parent : null;
|
|
3352
4240
|
if (!body) return;
|
|
3353
|
-
const playerPos = worldOf(body);
|
|
4241
|
+
const playerPos = worldOf$1(body);
|
|
3354
4242
|
const pivotRate = this.view === "free" ? 11 : 1e3;
|
|
3355
4243
|
const pk = 1 - Math.exp(-pivotRate * dt);
|
|
3356
4244
|
const pivot = this.pivot ?? [
|
|
@@ -3404,9 +4292,9 @@ var CharacterController3D = class extends Node3D {
|
|
|
3404
4292
|
eulerScratch.setFromQuaternion(quatScratch, "XYZ");
|
|
3405
4293
|
}
|
|
3406
4294
|
camera.rotation = [
|
|
3407
|
-
eulerScratch.x * RAD2DEG,
|
|
3408
|
-
eulerScratch.y * RAD2DEG,
|
|
3409
|
-
eulerScratch.z * RAD2DEG
|
|
4295
|
+
eulerScratch.x * RAD2DEG$3,
|
|
4296
|
+
eulerScratch.y * RAD2DEG$3,
|
|
4297
|
+
eulerScratch.z * RAD2DEG$3
|
|
3410
4298
|
];
|
|
3411
4299
|
}
|
|
3412
4300
|
pivot = null;
|
|
@@ -3428,9 +4316,368 @@ var CharacterController3D = class extends Node3D {
|
|
|
3428
4316
|
return this.cameraCache;
|
|
3429
4317
|
}
|
|
3430
4318
|
};
|
|
3431
|
-
function clamp(v, lo, hi) {
|
|
3432
|
-
return Math.min(hi, Math.max(lo, v));
|
|
3433
|
-
}
|
|
4319
|
+
function clamp(v, lo, hi) {
|
|
4320
|
+
return Math.min(hi, Math.max(lo, v));
|
|
4321
|
+
}
|
|
4322
|
+
function collectAreas(node, group, out) {
|
|
4323
|
+
if (node instanceof Area3D && node.isInGroup(group)) out.push(node);
|
|
4324
|
+
for (const child of node.children) collectAreas(child, group, out);
|
|
4325
|
+
}
|
|
4326
|
+
function collectWaters(node, out) {
|
|
4327
|
+
if (node instanceof Water3D) out.push(node);
|
|
4328
|
+
for (const child of node.children) collectWaters(child, out);
|
|
4329
|
+
}
|
|
4330
|
+
function worldOf$1(node) {
|
|
4331
|
+
let x = 0;
|
|
4332
|
+
let y = 0;
|
|
4333
|
+
let z = 0;
|
|
4334
|
+
for (let n = node; n; n = n.parent) {
|
|
4335
|
+
if (!(n instanceof Node3D)) break;
|
|
4336
|
+
x += n.position[0] ?? 0;
|
|
4337
|
+
y += n.position[1] ?? 0;
|
|
4338
|
+
z += n.position[2] ?? 0;
|
|
4339
|
+
}
|
|
4340
|
+
return [
|
|
4341
|
+
x,
|
|
4342
|
+
y,
|
|
4343
|
+
z
|
|
4344
|
+
];
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* The EDGE, not the level.
|
|
4348
|
+
*
|
|
4349
|
+
* Jump used to read `isPressed`, so holding the button re-jumped on every frame
|
|
4350
|
+
* the raycast found ground — an auto-hop nobody asked for, and one that makes
|
|
4351
|
+
* coyote time incoherent (a held button would re-fire through the whole
|
|
4352
|
+
* window). Undeclared actions are tolerated, and reported once.
|
|
4353
|
+
*/
|
|
4354
|
+
/** A node's world x/z — the platform may itself be parented to something. */
|
|
4355
|
+
function groundXZ(node) {
|
|
4356
|
+
const world = worldPosition(node);
|
|
4357
|
+
return [world[0] ?? 0, world[2] ?? 0];
|
|
4358
|
+
}
|
|
4359
|
+
/** An XZ offset turned by the rotation that takes `qFrom` to `qNow` (quaternions `[x,y,z,w]`). */
|
|
4360
|
+
function rotateXZByDelta(qNow, qFrom, ox, oz) {
|
|
4361
|
+
const [fx, fy, fz, fw] = [
|
|
4362
|
+
-(qFrom[0] ?? 0),
|
|
4363
|
+
-(qFrom[1] ?? 0),
|
|
4364
|
+
-(qFrom[2] ?? 0),
|
|
4365
|
+
qFrom[3] ?? 1
|
|
4366
|
+
];
|
|
4367
|
+
const [nx, ny, nz, nw] = [
|
|
4368
|
+
qNow[0] ?? 0,
|
|
4369
|
+
qNow[1] ?? 0,
|
|
4370
|
+
qNow[2] ?? 0,
|
|
4371
|
+
qNow[3] ?? 1
|
|
4372
|
+
];
|
|
4373
|
+
const dx = nw * fx + nx * fw + ny * fz - nz * fy;
|
|
4374
|
+
const dy = nw * fy - nx * fz + ny * fw + nz * fx;
|
|
4375
|
+
const dz = nw * fz + nx * fy - ny * fx + nz * fw;
|
|
4376
|
+
const dw = nw * fw - nx * fx - ny * fy - nz * fz;
|
|
4377
|
+
const oy = 0;
|
|
4378
|
+
const cx = dy * oz - dz * oy;
|
|
4379
|
+
const cy = dz * ox - dx * oz;
|
|
4380
|
+
const cz = dx * oy - dy * ox;
|
|
4381
|
+
return [
|
|
4382
|
+
ox + 2 * (dw * cx + (dy * cz - dz * cy)),
|
|
4383
|
+
oy + 2 * (dw * cy + (dz * cx - dx * cz)),
|
|
4384
|
+
oz + 2 * (dw * cz + (dx * cy - dy * cx))
|
|
4385
|
+
];
|
|
4386
|
+
}
|
|
4387
|
+
function hasPositionArray(n) {
|
|
4388
|
+
return Array.isArray(n.position);
|
|
4389
|
+
}
|
|
4390
|
+
//#endregion
|
|
4391
|
+
//#region src/3d/nodes/flock-3d.ts
|
|
4392
|
+
const RAD2DEG$2 = 180 / Math.PI;
|
|
4393
|
+
/**
|
|
4394
|
+
* A flock — gulls over a harbor, fish under a boat, bats out of a cave — as
|
|
4395
|
+
* ONE instanced mesh that flies its own instances.
|
|
4396
|
+
*
|
|
4397
|
+
* ```json
|
|
4398
|
+
* { "name": "Gulls", "type": "Flock3D",
|
|
4399
|
+
* "props": { "position": [0, 14, 0], "count": 40, "radius": 30, "height": 6,
|
|
4400
|
+
* "mesh": "box", "size": [0.5, 0.08, 0.3], "follow": "/root/Boat" } }
|
|
4401
|
+
* ```
|
|
4402
|
+
*
|
|
4403
|
+
* Boids (Reynolds 1987): each instance steers away from neighbours closer
|
|
4404
|
+
* than `separation`, toward the average heading of those within `sight`
|
|
4405
|
+
* (`alignment`) and toward their centre (`cohesion`), turns back inside an
|
|
4406
|
+
* ellipsoid `radius` wide and `height` tall around this node — or around the
|
|
4407
|
+
* node at `follow`, offset by this node's position — and never turns faster
|
|
4408
|
+
* than `turn`. Every instance flies at `speed`, +z forward, so a mesh that is
|
|
4409
|
+
* longer along z reads as a body with a beak.
|
|
4410
|
+
*
|
|
4411
|
+
* No physics bodies, no behaviour per bird: forty birds cost one draw call
|
|
4412
|
+
* and forty-squared distance checks a frame. Seeded from `engine.rng`, so a
|
|
4413
|
+
* seeded run flies the same flight. `boids()` answers headless.
|
|
4414
|
+
*/
|
|
4415
|
+
var Flock3D = class extends InstancedMesh3D {
|
|
4416
|
+
static typeName = "Flock3D";
|
|
4417
|
+
static props = {
|
|
4418
|
+
...InstancedMesh3D.props,
|
|
4419
|
+
/** How many. Live — a change reseeds the flock. */
|
|
4420
|
+
count: {
|
|
4421
|
+
default: 30,
|
|
4422
|
+
range: { min: 0 }
|
|
4423
|
+
},
|
|
4424
|
+
/** The bound they turn back from: half-width (m) around the centre… */
|
|
4425
|
+
radius: {
|
|
4426
|
+
default: 20,
|
|
4427
|
+
range: { min: .1 }
|
|
4428
|
+
},
|
|
4429
|
+
/** …and half-height (m). A flat ellipsoid keeps fish under the surface. */
|
|
4430
|
+
height: {
|
|
4431
|
+
default: 8,
|
|
4432
|
+
range: { min: .1 }
|
|
4433
|
+
},
|
|
4434
|
+
/** Every bird's pace (m/s). */
|
|
4435
|
+
speed: {
|
|
4436
|
+
default: 6,
|
|
4437
|
+
range: { min: 0 }
|
|
4438
|
+
},
|
|
4439
|
+
/** The sharpest turn (rad/s) — lower is lazier, gull-like. */
|
|
4440
|
+
turn: {
|
|
4441
|
+
default: 2.5,
|
|
4442
|
+
range: { min: 0 }
|
|
4443
|
+
},
|
|
4444
|
+
/** Closer than this (m) and two birds push apart. */
|
|
4445
|
+
separation: {
|
|
4446
|
+
default: 1.5,
|
|
4447
|
+
range: { min: 0 }
|
|
4448
|
+
},
|
|
4449
|
+
/** How far (m) a bird sees its neighbours for alignment and cohesion. */
|
|
4450
|
+
sight: {
|
|
4451
|
+
default: 5,
|
|
4452
|
+
range: { min: 0 }
|
|
4453
|
+
},
|
|
4454
|
+
/** Weight of matching the neighbours' heading (0 = none). */
|
|
4455
|
+
alignment: {
|
|
4456
|
+
default: 1,
|
|
4457
|
+
range: { min: 0 }
|
|
4458
|
+
},
|
|
4459
|
+
/** Weight of drifting toward the neighbours' centre (0 = none). */
|
|
4460
|
+
cohesion: {
|
|
4461
|
+
default: 1,
|
|
4462
|
+
range: { min: 0 }
|
|
4463
|
+
},
|
|
4464
|
+
/** A node the flock's centre follows, offset by this node's position; '' = this node. */
|
|
4465
|
+
follow: {
|
|
4466
|
+
default: "",
|
|
4467
|
+
nodePath: true
|
|
4468
|
+
}
|
|
4469
|
+
};
|
|
4470
|
+
count = 30;
|
|
4471
|
+
radius = 20;
|
|
4472
|
+
height = 8;
|
|
4473
|
+
speed = 6;
|
|
4474
|
+
turn = 2.5;
|
|
4475
|
+
separation = 1.5;
|
|
4476
|
+
sight = 5;
|
|
4477
|
+
alignment = 1;
|
|
4478
|
+
cohesion = 1;
|
|
4479
|
+
follow = "";
|
|
4480
|
+
px = new Float64Array(0);
|
|
4481
|
+
py = new Float64Array(0);
|
|
4482
|
+
pz = new Float64Array(0);
|
|
4483
|
+
vx = new Float64Array(0);
|
|
4484
|
+
vy = new Float64Array(0);
|
|
4485
|
+
vz = new Float64Array(0);
|
|
4486
|
+
rows = [];
|
|
4487
|
+
static validateJson(node) {
|
|
4488
|
+
InstancedMesh3D.validateJson(node);
|
|
4489
|
+
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" });
|
|
4490
|
+
}
|
|
4491
|
+
/** Every bird right now, in world metres and m/s. */
|
|
4492
|
+
boids() {
|
|
4493
|
+
const [ox, oy, oz] = worldOf(this);
|
|
4494
|
+
const out = [];
|
|
4495
|
+
for (let i = 0; i < this.px.length; i++) out.push({
|
|
4496
|
+
x: (this.px[i] ?? 0) + ox,
|
|
4497
|
+
y: (this.py[i] ?? 0) + oy,
|
|
4498
|
+
z: (this.pz[i] ?? 0) + oz,
|
|
4499
|
+
vx: this.vx[i] ?? 0,
|
|
4500
|
+
vy: this.vy[i] ?? 0,
|
|
4501
|
+
vz: this.vz[i] ?? 0
|
|
4502
|
+
});
|
|
4503
|
+
return out;
|
|
4504
|
+
}
|
|
4505
|
+
update(dt) {
|
|
4506
|
+
const engine = this.tree?.engine;
|
|
4507
|
+
if (!engine) return;
|
|
4508
|
+
const n = Math.max(0, Math.floor(this.count));
|
|
4509
|
+
if (this.px.length !== n) this.seed(engine.rng, n);
|
|
4510
|
+
if (n === 0) {
|
|
4511
|
+
if (this.transforms.length !== 0) this.transforms = [];
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
const [cx, cy, cz] = this.centre();
|
|
4515
|
+
const step = Math.min(dt, 1 / 20);
|
|
4516
|
+
const sight2 = this.sight * this.sight;
|
|
4517
|
+
const sep2 = this.separation * this.separation;
|
|
4518
|
+
const maxTurn = this.turn * step;
|
|
4519
|
+
const { px, py, pz, vx, vy, vz } = this;
|
|
4520
|
+
for (let i = 0; i < n; i++) {
|
|
4521
|
+
const x = px[i] ?? 0;
|
|
4522
|
+
const y = py[i] ?? 0;
|
|
4523
|
+
const z = pz[i] ?? 0;
|
|
4524
|
+
let sx = 0;
|
|
4525
|
+
let sy = 0;
|
|
4526
|
+
let sz = 0;
|
|
4527
|
+
let ax = 0;
|
|
4528
|
+
let ay = 0;
|
|
4529
|
+
let az = 0;
|
|
4530
|
+
let mx = 0;
|
|
4531
|
+
let my = 0;
|
|
4532
|
+
let mz = 0;
|
|
4533
|
+
let seen = 0;
|
|
4534
|
+
for (let j = 0; j < n; j++) {
|
|
4535
|
+
if (j === i) continue;
|
|
4536
|
+
const dx = (px[j] ?? 0) - x;
|
|
4537
|
+
const dy = (py[j] ?? 0) - y;
|
|
4538
|
+
const dz = (pz[j] ?? 0) - z;
|
|
4539
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
4540
|
+
if (d2 > sight2) continue;
|
|
4541
|
+
seen += 1;
|
|
4542
|
+
ax += vx[j] ?? 0;
|
|
4543
|
+
ay += vy[j] ?? 0;
|
|
4544
|
+
az += vz[j] ?? 0;
|
|
4545
|
+
mx += dx;
|
|
4546
|
+
my += dy;
|
|
4547
|
+
mz += dz;
|
|
4548
|
+
if (d2 < sep2 && d2 > 1e-9) {
|
|
4549
|
+
const inv = 1 / d2;
|
|
4550
|
+
sx -= dx * inv;
|
|
4551
|
+
sy -= dy * inv;
|
|
4552
|
+
sz -= dz * inv;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
let fx = sx * this.speed * 2;
|
|
4556
|
+
let fy = sy * this.speed * 2;
|
|
4557
|
+
let fz = sz * this.speed * 2;
|
|
4558
|
+
if (seen > 0) {
|
|
4559
|
+
fx += (ax / seen - (vx[i] ?? 0)) * this.alignment / .5;
|
|
4560
|
+
fy += (ay / seen - (vy[i] ?? 0)) * this.alignment / .5;
|
|
4561
|
+
fz += (az / seen - (vz[i] ?? 0)) * this.alignment / .5;
|
|
4562
|
+
fx += mx / seen * this.cohesion * 2;
|
|
4563
|
+
fy += my / seen * this.cohesion * 2;
|
|
4564
|
+
fz += mz / seen * this.cohesion * 2;
|
|
4565
|
+
}
|
|
4566
|
+
const ex = (x - cx) / this.radius;
|
|
4567
|
+
const ey = (y - cy) / this.height;
|
|
4568
|
+
const ez = (z - cz) / this.radius;
|
|
4569
|
+
const m = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
4570
|
+
if (m > .6) {
|
|
4571
|
+
const k = (m - .6) / .4 * this.speed * 8;
|
|
4572
|
+
const gx = ex / this.radius;
|
|
4573
|
+
const gy = ey / this.height;
|
|
4574
|
+
const gz = ez / this.radius;
|
|
4575
|
+
const g = Math.hypot(gx, gy, gz) || 1;
|
|
4576
|
+
fx -= gx / g * k;
|
|
4577
|
+
fy -= gy / g * k;
|
|
4578
|
+
fz -= gz / g * k;
|
|
4579
|
+
}
|
|
4580
|
+
let nx = (vx[i] ?? 0) + fx * step;
|
|
4581
|
+
let ny = (vy[i] ?? 0) + fy * step;
|
|
4582
|
+
let nz = (vz[i] ?? 0) + fz * step;
|
|
4583
|
+
const ov = Math.hypot(vx[i] ?? 0, vy[i] ?? 0, vz[i] ?? 0) || 1;
|
|
4584
|
+
const nv = Math.hypot(nx, ny, nz) || 1;
|
|
4585
|
+
const cos = ((vx[i] ?? 0) * nx + (vy[i] ?? 0) * ny + (vz[i] ?? 0) * nz) / (ov * nv);
|
|
4586
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, cos)));
|
|
4587
|
+
if (angle > maxTurn && angle > 1e-6) {
|
|
4588
|
+
const t = maxTurn / angle;
|
|
4589
|
+
nx = (vx[i] ?? 0) / ov * (1 - t) + nx / nv * t;
|
|
4590
|
+
ny = (vy[i] ?? 0) / ov * (1 - t) + ny / nv * t;
|
|
4591
|
+
nz = (vz[i] ?? 0) / ov * (1 - t) + nz / nv * t;
|
|
4592
|
+
}
|
|
4593
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
4594
|
+
vx[i] = nx / len * this.speed;
|
|
4595
|
+
vy[i] = ny / len * this.speed;
|
|
4596
|
+
vz[i] = nz / len * this.speed;
|
|
4597
|
+
}
|
|
4598
|
+
for (let i = 0; i < n; i++) {
|
|
4599
|
+
px[i] = (px[i] ?? 0) + (vx[i] ?? 0) * step;
|
|
4600
|
+
py[i] = (py[i] ?? 0) + (vy[i] ?? 0) * step;
|
|
4601
|
+
pz[i] = (pz[i] ?? 0) + (vz[i] ?? 0) * step;
|
|
4602
|
+
let row = this.rows[i];
|
|
4603
|
+
if (!row) {
|
|
4604
|
+
row = [
|
|
4605
|
+
0,
|
|
4606
|
+
0,
|
|
4607
|
+
0,
|
|
4608
|
+
0,
|
|
4609
|
+
1
|
|
4610
|
+
];
|
|
4611
|
+
this.rows[i] = row;
|
|
4612
|
+
}
|
|
4613
|
+
row[0] = px[i] ?? 0;
|
|
4614
|
+
row[1] = py[i] ?? 0;
|
|
4615
|
+
row[2] = pz[i] ?? 0;
|
|
4616
|
+
row[3] = Math.atan2(vx[i] ?? 0, vz[i] ?? 0) * RAD2DEG$2;
|
|
4617
|
+
}
|
|
4618
|
+
this.transforms = this.rows;
|
|
4619
|
+
}
|
|
4620
|
+
/** The roam centre in this node's local frame: the followed node's offset, or the origin. */
|
|
4621
|
+
centre() {
|
|
4622
|
+
if (this.follow === "") return [
|
|
4623
|
+
0,
|
|
4624
|
+
0,
|
|
4625
|
+
0
|
|
4626
|
+
];
|
|
4627
|
+
const target = this.getNodeOrNull(this.follow);
|
|
4628
|
+
if (!(target instanceof Node3D)) return [
|
|
4629
|
+
0,
|
|
4630
|
+
0,
|
|
4631
|
+
0
|
|
4632
|
+
];
|
|
4633
|
+
const [tx, ty, tz] = worldOf(target);
|
|
4634
|
+
const [ox, oy, oz] = worldOf(this);
|
|
4635
|
+
const [lx, ly, lz] = [
|
|
4636
|
+
this.position[0] ?? 0,
|
|
4637
|
+
this.position[1] ?? 0,
|
|
4638
|
+
this.position[2] ?? 0
|
|
4639
|
+
];
|
|
4640
|
+
return [
|
|
4641
|
+
tx - ox + lx,
|
|
4642
|
+
ty - oy + ly,
|
|
4643
|
+
tz - oz + lz
|
|
4644
|
+
];
|
|
4645
|
+
}
|
|
4646
|
+
/** Scatter `n` birds through the bound with random headings — engine-seeded. */
|
|
4647
|
+
seed(rng, n) {
|
|
4648
|
+
this.px = new Float64Array(n);
|
|
4649
|
+
this.py = new Float64Array(n);
|
|
4650
|
+
this.pz = new Float64Array(n);
|
|
4651
|
+
this.vx = new Float64Array(n);
|
|
4652
|
+
this.vy = new Float64Array(n);
|
|
4653
|
+
this.vz = new Float64Array(n);
|
|
4654
|
+
this.rows.length = n;
|
|
4655
|
+
const [cx, cy, cz] = this.centre();
|
|
4656
|
+
for (let i = 0; i < n; i++) {
|
|
4657
|
+
const u = rng.next();
|
|
4658
|
+
const r = .6 * Math.cbrt(u);
|
|
4659
|
+
const theta = rng.next() * Math.PI * 2;
|
|
4660
|
+
const phi = Math.acos(2 * rng.next() - 1);
|
|
4661
|
+
this.px[i] = cx + r * this.radius * Math.sin(phi) * Math.cos(theta);
|
|
4662
|
+
this.py[i] = cy + r * this.height * Math.cos(phi);
|
|
4663
|
+
this.pz[i] = cz + r * this.radius * Math.sin(phi) * Math.sin(theta);
|
|
4664
|
+
const h = rng.next() * Math.PI * 2;
|
|
4665
|
+
const pitch = (rng.next() - .5) * .4;
|
|
4666
|
+
this.vx[i] = Math.cos(pitch) * Math.sin(h) * this.speed;
|
|
4667
|
+
this.vy[i] = Math.sin(pitch) * this.speed;
|
|
4668
|
+
this.vz[i] = Math.cos(pitch) * Math.cos(h) * this.speed;
|
|
4669
|
+
this.rows[i] = [
|
|
4670
|
+
this.px[i] ?? 0,
|
|
4671
|
+
this.py[i] ?? 0,
|
|
4672
|
+
this.pz[i] ?? 0,
|
|
4673
|
+
h * RAD2DEG$2,
|
|
4674
|
+
1
|
|
4675
|
+
];
|
|
4676
|
+
}
|
|
4677
|
+
this.transforms = this.rows;
|
|
4678
|
+
}
|
|
4679
|
+
};
|
|
4680
|
+
/** Translation-only world position (the engine's physics rule). */
|
|
3434
4681
|
function worldOf(node) {
|
|
3435
4682
|
let x = 0;
|
|
3436
4683
|
let y = 0;
|
|
@@ -3447,19 +4694,6 @@ function worldOf(node) {
|
|
|
3447
4694
|
z
|
|
3448
4695
|
];
|
|
3449
4696
|
}
|
|
3450
|
-
/**
|
|
3451
|
-
* The EDGE, not the level.
|
|
3452
|
-
*
|
|
3453
|
-
* Jump used to read `isPressed`, so holding the button re-jumped on every frame
|
|
3454
|
-
* the raycast found ground — an auto-hop nobody asked for, and one that makes
|
|
3455
|
-
* coyote time incoherent (a held button would re-fire through the whole
|
|
3456
|
-
* window). Undeclared actions are tolerated, and reported once.
|
|
3457
|
-
*/
|
|
3458
|
-
/** A node's world x/z — the platform may itself be parented to something. */
|
|
3459
|
-
function groundXZ(node) {
|
|
3460
|
-
const world = worldPosition(node);
|
|
3461
|
-
return [world[0] ?? 0, world[2] ?? 0];
|
|
3462
|
-
}
|
|
3463
4697
|
//#endregion
|
|
3464
4698
|
//#region src/3d/vegetation/grass-placement.ts
|
|
3465
4699
|
const NOISE_SCALE = .15;
|
|
@@ -7625,7 +8859,19 @@ var Particles3D = class Particles3D extends Node3D {
|
|
|
7625
8859
|
options: ["add", "normal"]
|
|
7626
8860
|
},
|
|
7627
8861
|
depthTest: { default: true },
|
|
7628
|
-
maxParticles: { default: 256 }
|
|
8862
|
+
maxParticles: { default: 256 },
|
|
8863
|
+
/** Half-extents (m) of a box the particles are born in, around the node — a cloud, a dusty field; `[0,0,0]` = a point. */
|
|
8864
|
+
emitBox: { default: [
|
|
8865
|
+
0,
|
|
8866
|
+
0,
|
|
8867
|
+
0
|
|
8868
|
+
] },
|
|
8869
|
+
/** A steady velocity (px/s, y+ = down like `gravity`) every particle is born with — wind on smoke. */
|
|
8870
|
+
drift: { default: [
|
|
8871
|
+
0,
|
|
8872
|
+
0,
|
|
8873
|
+
0
|
|
8874
|
+
] }
|
|
7629
8875
|
};
|
|
7630
8876
|
preset = "custom";
|
|
7631
8877
|
emitting = true;
|
|
@@ -7682,6 +8928,20 @@ var Particles3D = class Particles3D extends Node3D {
|
|
|
7682
8928
|
});
|
|
7683
8929
|
}
|
|
7684
8930
|
sim = null;
|
|
8931
|
+
emitBox = [
|
|
8932
|
+
0,
|
|
8933
|
+
0,
|
|
8934
|
+
0
|
|
8935
|
+
];
|
|
8936
|
+
drift = [
|
|
8937
|
+
0,
|
|
8938
|
+
0,
|
|
8939
|
+
0
|
|
8940
|
+
];
|
|
8941
|
+
/** @internal Forget the sim so the next update rebuilds it from the props (a preset or kind that changed). */
|
|
8942
|
+
_rebuildSim() {
|
|
8943
|
+
this.sim = null;
|
|
8944
|
+
}
|
|
7685
8945
|
bursted = false;
|
|
7686
8946
|
finishedEmitted = false;
|
|
7687
8947
|
/** @internal */
|
|
@@ -7702,7 +8962,17 @@ var Particles3D = class Particles3D extends Node3D {
|
|
|
7702
8962
|
],
|
|
7703
8963
|
drag: this.drag,
|
|
7704
8964
|
maxParticles: this.maxParticles,
|
|
7705
|
-
spreadZ: true
|
|
8965
|
+
spreadZ: true,
|
|
8966
|
+
emitBox: [
|
|
8967
|
+
this.emitBox[0] ?? 0,
|
|
8968
|
+
this.emitBox[1] ?? 0,
|
|
8969
|
+
this.emitBox[2] ?? 0
|
|
8970
|
+
],
|
|
8971
|
+
drift: [
|
|
8972
|
+
(this.drift[0] ?? 0) * PX_TO_M,
|
|
8973
|
+
(this.drift[1] ?? 0) * PX_TO_M,
|
|
8974
|
+
(this.drift[2] ?? 0) * PX_TO_M
|
|
8975
|
+
]
|
|
7706
8976
|
}, rng);
|
|
7707
8977
|
}
|
|
7708
8978
|
return this.sim;
|
|
@@ -7824,9 +9094,24 @@ var Particles3D = class Particles3D extends Node3D {
|
|
|
7824
9094
|
}
|
|
7825
9095
|
return this.paletteCache;
|
|
7826
9096
|
}
|
|
9097
|
+
/**
|
|
9098
|
+
* The stamp every particle is drawn with — a soft round dot.
|
|
9099
|
+
*
|
|
9100
|
+
* A subclass overrides it to look like the thing it IS: `Weather3D`'s rain is
|
|
9101
|
+
* a vertical STREAK, because 2,600 round dots read as a dirty lens and not as
|
|
9102
|
+
* a downpour (measured in a browser at intensity 1). The material is rebuilt
|
|
9103
|
+
* when this changes, so a node may return a different stamp over its life.
|
|
9104
|
+
*/
|
|
9105
|
+
sprite() {
|
|
9106
|
+
return softSprite();
|
|
9107
|
+
}
|
|
9108
|
+
/** The stamp the current material was built with, so a change can rebuild it. */
|
|
9109
|
+
builtSprite = null;
|
|
7827
9110
|
syncParticles() {
|
|
7828
9111
|
const sim = this._ensureSim();
|
|
7829
|
-
|
|
9112
|
+
const stamp = this.sprite();
|
|
9113
|
+
if (!this.points || (this.positions?.length ?? 0) !== this.maxParticles * 3 || this.builtSprite !== stamp) {
|
|
9114
|
+
this.builtSprite = stamp;
|
|
7830
9115
|
this.points?.removeFromParent();
|
|
7831
9116
|
this.positions = new Float32Array(this.maxParticles * 3);
|
|
7832
9117
|
this.colors = new Float32Array(this.maxParticles * 4);
|
|
@@ -7835,7 +9120,7 @@ var Particles3D = class Particles3D extends Node3D {
|
|
|
7835
9120
|
geometry.setAttribute("color", new BufferAttribute(this.colors, 4));
|
|
7836
9121
|
const material = new PointsMaterial({
|
|
7837
9122
|
size: (this.sizeStart + this.sizeEnd) / 2 * PX_TO_M,
|
|
7838
|
-
map:
|
|
9123
|
+
map: this.sprite(),
|
|
7839
9124
|
vertexColors: true,
|
|
7840
9125
|
transparent: true,
|
|
7841
9126
|
depthWrite: false,
|
|
@@ -7942,6 +9227,356 @@ function softSprite() {
|
|
|
7942
9227
|
return tex;
|
|
7943
9228
|
}
|
|
7944
9229
|
//#endregion
|
|
9230
|
+
//#region src/3d/nodes/ragdoll-3d.ts
|
|
9231
|
+
const RAD2DEG$1 = 180 / Math.PI;
|
|
9232
|
+
const DEG2RAD = Math.PI / 180;
|
|
9233
|
+
/**
|
|
9234
|
+
* The Mixamo rig every agent8 character wears, as ten capsules. Parents
|
|
9235
|
+
* come before children so a joint's target is already in the tree. A limb
|
|
9236
|
+
* whose `to` bone is missing is skipped — except the head, which becomes a
|
|
9237
|
+
* 0.2 m ball when the rig has no `HeadTop_End`.
|
|
9238
|
+
*/
|
|
9239
|
+
const RIG = [
|
|
9240
|
+
{
|
|
9241
|
+
name: "Torso",
|
|
9242
|
+
from: "Hips",
|
|
9243
|
+
to: "Neck",
|
|
9244
|
+
radius: .14,
|
|
9245
|
+
share: .32,
|
|
9246
|
+
parent: null
|
|
9247
|
+
},
|
|
9248
|
+
{
|
|
9249
|
+
name: "Head",
|
|
9250
|
+
from: "Head",
|
|
9251
|
+
to: "HeadTop_End",
|
|
9252
|
+
radius: .1,
|
|
9253
|
+
share: .08,
|
|
9254
|
+
parent: "Torso"
|
|
9255
|
+
},
|
|
9256
|
+
{
|
|
9257
|
+
name: "LeftUpperArm",
|
|
9258
|
+
from: "LeftArm",
|
|
9259
|
+
to: "LeftForeArm",
|
|
9260
|
+
radius: .05,
|
|
9261
|
+
share: .05,
|
|
9262
|
+
parent: "Torso"
|
|
9263
|
+
},
|
|
9264
|
+
{
|
|
9265
|
+
name: "LeftForearm",
|
|
9266
|
+
from: "LeftForeArm",
|
|
9267
|
+
to: "LeftHand",
|
|
9268
|
+
radius: .045,
|
|
9269
|
+
share: .04,
|
|
9270
|
+
parent: "LeftUpperArm"
|
|
9271
|
+
},
|
|
9272
|
+
{
|
|
9273
|
+
name: "RightUpperArm",
|
|
9274
|
+
from: "RightArm",
|
|
9275
|
+
to: "RightForeArm",
|
|
9276
|
+
radius: .05,
|
|
9277
|
+
share: .05,
|
|
9278
|
+
parent: "Torso"
|
|
9279
|
+
},
|
|
9280
|
+
{
|
|
9281
|
+
name: "RightForearm",
|
|
9282
|
+
from: "RightForeArm",
|
|
9283
|
+
to: "RightHand",
|
|
9284
|
+
radius: .045,
|
|
9285
|
+
share: .04,
|
|
9286
|
+
parent: "RightUpperArm"
|
|
9287
|
+
},
|
|
9288
|
+
{
|
|
9289
|
+
name: "LeftThigh",
|
|
9290
|
+
from: "LeftUpLeg",
|
|
9291
|
+
to: "LeftLeg",
|
|
9292
|
+
radius: .08,
|
|
9293
|
+
share: .12,
|
|
9294
|
+
parent: "Torso"
|
|
9295
|
+
},
|
|
9296
|
+
{
|
|
9297
|
+
name: "LeftShin",
|
|
9298
|
+
from: "LeftLeg",
|
|
9299
|
+
to: "LeftFoot",
|
|
9300
|
+
radius: .06,
|
|
9301
|
+
share: .07,
|
|
9302
|
+
parent: "LeftThigh"
|
|
9303
|
+
},
|
|
9304
|
+
{
|
|
9305
|
+
name: "RightThigh",
|
|
9306
|
+
from: "RightUpLeg",
|
|
9307
|
+
to: "RightLeg",
|
|
9308
|
+
radius: .08,
|
|
9309
|
+
share: .12,
|
|
9310
|
+
parent: "Torso"
|
|
9311
|
+
},
|
|
9312
|
+
{
|
|
9313
|
+
name: "RightShin",
|
|
9314
|
+
from: "RightLeg",
|
|
9315
|
+
to: "RightFoot",
|
|
9316
|
+
radius: .06,
|
|
9317
|
+
share: .07,
|
|
9318
|
+
parent: "RightThigh"
|
|
9319
|
+
}
|
|
9320
|
+
];
|
|
9321
|
+
const scratchM = new Matrix4();
|
|
9322
|
+
const scratchM2 = new Matrix4();
|
|
9323
|
+
const scratchQ = new Quaternion();
|
|
9324
|
+
const scratchE = new Euler();
|
|
9325
|
+
const scratchV = new Vector3();
|
|
9326
|
+
const scratchS = new Vector3(1, 1, 1);
|
|
9327
|
+
/**
|
|
9328
|
+
* A ragdoll for a skinned model — the death that falls.
|
|
9329
|
+
*
|
|
9330
|
+
* ```json
|
|
9331
|
+
* { "name": "Body", "type": "CharacterBody3D", "children": [
|
|
9332
|
+
* { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$base" } },
|
|
9333
|
+
* { "name": "Ragdoll", "type": "Ragdoll3D", "props": { "target": "../Skin", "lifetime": 8 } }
|
|
9334
|
+
* ] }
|
|
9335
|
+
* ```
|
|
9336
|
+
*
|
|
9337
|
+
* Idle it is nothing. `activate()` reads the skeleton at `target` (Mixamo
|
|
9338
|
+
* names, forgiving lookup), builds a capsule `RigidBody3D` per limb where the
|
|
9339
|
+
* bones are, joins each to its parent with a spherical `Joint3D`, gives them
|
|
9340
|
+
* the velocity of the body they fell out of, and from then on writes the
|
|
9341
|
+
* bones' world matrices from the limbs every frame — the model crumples,
|
|
9342
|
+
* slides, hangs off the ledge. Wire it from `Health.died`; turn the body's
|
|
9343
|
+
* collider and its controller off beside it. `reset()` puts the skeleton
|
|
9344
|
+
* back under the animation; `lifetime` does that on its own.
|
|
9345
|
+
*
|
|
9346
|
+
* Headless there is no skeleton: `activate(pose)` takes bone positions, so a
|
|
9347
|
+
* test can ask the same questions the game will.
|
|
9348
|
+
*/
|
|
9349
|
+
var Ragdoll3D = class extends Node3D {
|
|
9350
|
+
static typeName = "Ragdoll3D";
|
|
9351
|
+
static props = {
|
|
9352
|
+
/** The ModelInstance3D whose skeleton this is. */
|
|
9353
|
+
target: {
|
|
9354
|
+
default: "../Skin",
|
|
9355
|
+
nodePath: true
|
|
9356
|
+
},
|
|
9357
|
+
/** The whole body's mass (kg), shared out by limb. */
|
|
9358
|
+
mass: {
|
|
9359
|
+
default: 70,
|
|
9360
|
+
range: { min: 0 }
|
|
9361
|
+
},
|
|
9362
|
+
/** Angular damping on every limb — how quickly the flailing dies. */
|
|
9363
|
+
damping: {
|
|
9364
|
+
default: 3,
|
|
9365
|
+
range: { min: 0 }
|
|
9366
|
+
},
|
|
9367
|
+
/** Seconds before the ragdoll frees itself and hands the skeleton back; 0 = never. */
|
|
9368
|
+
lifetime: {
|
|
9369
|
+
default: 0,
|
|
9370
|
+
range: { min: 0 }
|
|
9371
|
+
},
|
|
9372
|
+
/** Limbs collide with each other (adjacent ones never do). */
|
|
9373
|
+
collide: { default: true }
|
|
9374
|
+
};
|
|
9375
|
+
static signals = ["activated", "reset"];
|
|
9376
|
+
target = "../Skin";
|
|
9377
|
+
mass = 70;
|
|
9378
|
+
damping = 3;
|
|
9379
|
+
lifetime = 0;
|
|
9380
|
+
collide = true;
|
|
9381
|
+
/** Read-only: built and driving the bones. */
|
|
9382
|
+
active = false;
|
|
9383
|
+
driven = [];
|
|
9384
|
+
limbNodes = [];
|
|
9385
|
+
left = 0;
|
|
9386
|
+
/** Every limb built right now, empty while idle. */
|
|
9387
|
+
limbs() {
|
|
9388
|
+
return this.limbNodes.slice();
|
|
9389
|
+
}
|
|
9390
|
+
/**
|
|
9391
|
+
* Build the ragdoll where the skeleton is (or where `pose` says the bones
|
|
9392
|
+
* are, headless). Returns false when there is nothing to build from.
|
|
9393
|
+
*/
|
|
9394
|
+
activate(pose = void 0) {
|
|
9395
|
+
if (this.active) return true;
|
|
9396
|
+
const root = this.getRoot();
|
|
9397
|
+
const model = this.getNodeOrNull(this.target);
|
|
9398
|
+
const bones = /* @__PURE__ */ new Map();
|
|
9399
|
+
const points = {};
|
|
9400
|
+
if (pose) for (const [k, v] of Object.entries(pose)) points[k] = v;
|
|
9401
|
+
else if (model && typeof model.findBone === "function") for (const spec of RIG) for (const name of [spec.from, spec.to]) {
|
|
9402
|
+
if (points[name]) continue;
|
|
9403
|
+
const bone = model.findBone(name);
|
|
9404
|
+
if (!bone) continue;
|
|
9405
|
+
bone.updateWorldMatrix(true, false);
|
|
9406
|
+
scratchV.setFromMatrixPosition(bone.matrixWorld);
|
|
9407
|
+
points[name] = [
|
|
9408
|
+
scratchV.x,
|
|
9409
|
+
scratchV.y,
|
|
9410
|
+
scratchV.z
|
|
9411
|
+
];
|
|
9412
|
+
bones.set(name, bone);
|
|
9413
|
+
}
|
|
9414
|
+
if (!points.Hips) {
|
|
9415
|
+
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.`);
|
|
9416
|
+
return false;
|
|
9417
|
+
}
|
|
9418
|
+
const velocity = this.hostVelocity();
|
|
9419
|
+
const built = /* @__PURE__ */ new Map();
|
|
9420
|
+
for (const spec of RIG) {
|
|
9421
|
+
const from = points[spec.from];
|
|
9422
|
+
if (!from) continue;
|
|
9423
|
+
let to = points[spec.to];
|
|
9424
|
+
if (!to) {
|
|
9425
|
+
if (spec.name !== "Head") continue;
|
|
9426
|
+
to = [
|
|
9427
|
+
from[0],
|
|
9428
|
+
from[1] + .2,
|
|
9429
|
+
from[2]
|
|
9430
|
+
];
|
|
9431
|
+
}
|
|
9432
|
+
const parent = spec.parent ? built.get(spec.parent) : null;
|
|
9433
|
+
if (spec.parent && !parent) continue;
|
|
9434
|
+
const dir = new Vector3(to[0] - from[0], to[1] - from[1], to[2] - from[2]);
|
|
9435
|
+
const len = Math.max(dir.length(), .05);
|
|
9436
|
+
dir.normalize();
|
|
9437
|
+
const centre = new Vector3(from[0] + dir.x * (len / 2), from[1] + dir.y * (len / 2), from[2] + dir.z * (len / 2));
|
|
9438
|
+
const rz = Math.asin(Math.max(-1, Math.min(1, -dir.x)));
|
|
9439
|
+
const cz = Math.cos(rz);
|
|
9440
|
+
const rx = cz > 1e-4 ? Math.atan2(dir.z / cz, dir.y / cz) : 0;
|
|
9441
|
+
const quat = new Quaternion().setFromEuler(new Euler(rx, 0, rz, "XYZ"));
|
|
9442
|
+
const radius = Math.min(spec.radius, len / 2 - .005);
|
|
9443
|
+
const json = {
|
|
9444
|
+
name: `${this.parent?.name ?? "Ragdoll"}${spec.name}`,
|
|
9445
|
+
type: "RigidBody3D",
|
|
9446
|
+
groups: ["ragdoll"],
|
|
9447
|
+
props: {
|
|
9448
|
+
position: [
|
|
9449
|
+
centre.x,
|
|
9450
|
+
centre.y,
|
|
9451
|
+
centre.z
|
|
9452
|
+
],
|
|
9453
|
+
rotation: [
|
|
9454
|
+
rx * RAD2DEG$1,
|
|
9455
|
+
0,
|
|
9456
|
+
rz * RAD2DEG$1
|
|
9457
|
+
],
|
|
9458
|
+
mass: Math.max(.5, this.mass * spec.share),
|
|
9459
|
+
angularDamping: this.damping,
|
|
9460
|
+
linearDamping: .3,
|
|
9461
|
+
friction: .8,
|
|
9462
|
+
collider: {
|
|
9463
|
+
shape: "capsule",
|
|
9464
|
+
radius,
|
|
9465
|
+
height: Math.max(.02, len - 2 * radius)
|
|
9466
|
+
},
|
|
9467
|
+
linearVelocity: velocity
|
|
9468
|
+
}
|
|
9469
|
+
};
|
|
9470
|
+
if (parent) {
|
|
9471
|
+
const local = new Vector3(from[0], from[1], from[2]).sub(parent.centre);
|
|
9472
|
+
local.applyQuaternion(parent.quat.clone().invert());
|
|
9473
|
+
const children = [];
|
|
9474
|
+
json.children = children;
|
|
9475
|
+
children.push({
|
|
9476
|
+
name: "Joint",
|
|
9477
|
+
type: "Joint3D",
|
|
9478
|
+
props: {
|
|
9479
|
+
type: "spherical",
|
|
9480
|
+
target: parent.node.getPath(),
|
|
9481
|
+
anchor: [
|
|
9482
|
+
0,
|
|
9483
|
+
-len / 2,
|
|
9484
|
+
0
|
|
9485
|
+
],
|
|
9486
|
+
targetAnchor: [
|
|
9487
|
+
local.x,
|
|
9488
|
+
local.y,
|
|
9489
|
+
local.z
|
|
9490
|
+
],
|
|
9491
|
+
collide: false
|
|
9492
|
+
}
|
|
9493
|
+
});
|
|
9494
|
+
}
|
|
9495
|
+
const node = buildNodeJson(json, rebuildOptions(this));
|
|
9496
|
+
root.addChild(node);
|
|
9497
|
+
built.set(spec.name, {
|
|
9498
|
+
node,
|
|
9499
|
+
centre,
|
|
9500
|
+
quat,
|
|
9501
|
+
len
|
|
9502
|
+
});
|
|
9503
|
+
this.limbNodes.push({
|
|
9504
|
+
name: spec.name,
|
|
9505
|
+
body: node
|
|
9506
|
+
});
|
|
9507
|
+
const bone = bones.get(spec.from) ?? null;
|
|
9508
|
+
const offset = new Matrix4();
|
|
9509
|
+
if (bone) {
|
|
9510
|
+
scratchM.compose(centre, quat, scratchS);
|
|
9511
|
+
offset.copy(scratchM).invert().multiply(bone.matrixWorld);
|
|
9512
|
+
bone.matrixWorldAutoUpdate = false;
|
|
9513
|
+
}
|
|
9514
|
+
this.driven.push({
|
|
9515
|
+
body: node,
|
|
9516
|
+
bone,
|
|
9517
|
+
offset
|
|
9518
|
+
});
|
|
9519
|
+
}
|
|
9520
|
+
if (this.limbNodes.length === 0) return false;
|
|
9521
|
+
this.active = true;
|
|
9522
|
+
this.left = this.lifetime;
|
|
9523
|
+
this.emit("activated");
|
|
9524
|
+
return true;
|
|
9525
|
+
}
|
|
9526
|
+
/** Free the limbs and hand the skeleton back to the animation. */
|
|
9527
|
+
reset() {
|
|
9528
|
+
if (!this.active) return;
|
|
9529
|
+
for (const d of this.driven) {
|
|
9530
|
+
if (d.bone) d.bone.matrixWorldAutoUpdate = true;
|
|
9531
|
+
if (d.body.tree !== null) d.body.queueFree();
|
|
9532
|
+
}
|
|
9533
|
+
this.driven = [];
|
|
9534
|
+
this.limbNodes = [];
|
|
9535
|
+
this.active = false;
|
|
9536
|
+
this.emit("reset");
|
|
9537
|
+
}
|
|
9538
|
+
update(dt) {
|
|
9539
|
+
if (!this.active) return;
|
|
9540
|
+
if (this.lifetime > 0) {
|
|
9541
|
+
this.left -= dt;
|
|
9542
|
+
if (this.left <= 0) {
|
|
9543
|
+
this.reset();
|
|
9544
|
+
return;
|
|
9545
|
+
}
|
|
9546
|
+
}
|
|
9547
|
+
for (const d of this.driven) {
|
|
9548
|
+
if (!d.bone) continue;
|
|
9549
|
+
const p = d.body.position;
|
|
9550
|
+
const r = d.body.rotation;
|
|
9551
|
+
scratchV.set(p[0] ?? 0, p[1] ?? 0, p[2] ?? 0);
|
|
9552
|
+
scratchQ.setFromEuler(scratchE.set((r[0] ?? 0) * DEG2RAD, (r[1] ?? 0) * DEG2RAD, (r[2] ?? 0) * DEG2RAD, "XYZ"));
|
|
9553
|
+
scratchM.compose(scratchV, scratchQ, scratchS);
|
|
9554
|
+
scratchM2.copy(scratchM).multiply(d.offset);
|
|
9555
|
+
d.bone.matrixWorld.copy(scratchM2);
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
free() {
|
|
9559
|
+
this.reset();
|
|
9560
|
+
super.free();
|
|
9561
|
+
}
|
|
9562
|
+
/** The velocity of the body this ragdoll falls out of — the first body up the tree. */
|
|
9563
|
+
hostVelocity() {
|
|
9564
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
9565
|
+
const v = n.linearVelocity ?? n.velocity;
|
|
9566
|
+
if (Array.isArray(v)) return [
|
|
9567
|
+
v[0] ?? 0,
|
|
9568
|
+
v[1] ?? 0,
|
|
9569
|
+
v[2] ?? 0
|
|
9570
|
+
];
|
|
9571
|
+
}
|
|
9572
|
+
return [
|
|
9573
|
+
0,
|
|
9574
|
+
0,
|
|
9575
|
+
0
|
|
9576
|
+
];
|
|
9577
|
+
}
|
|
9578
|
+
};
|
|
9579
|
+
//#endregion
|
|
7945
9580
|
//#region src/3d/water/river.ts
|
|
7946
9581
|
/**
|
|
7947
9582
|
* Self-carved bed. Sized off the WATER COLUMN, never off the width: a channel's
|
|
@@ -8894,6 +10529,15 @@ uniform float uRippleStrength;
|
|
|
8894
10529
|
uniform float uFlowRate;
|
|
8895
10530
|
uniform float uSparkle;
|
|
8896
10531
|
uniform float uFresnelScale;
|
|
10532
|
+
// the river's own sky cube (river-3d.ts): what the surface mirrors instead of
|
|
10533
|
+
// the painted three-colour gradient, which stays as the headless fallback
|
|
10534
|
+
uniform samplerCube uEnvironmentMap;
|
|
10535
|
+
uniform float uUseEnvironmentMap;
|
|
10536
|
+
// the live ripple layer (fft-gpu.ts), a 6 m patch in the ribbon's own frame
|
|
10537
|
+
uniform float uRippleOn;
|
|
10538
|
+
uniform sampler2D uRippleNormal;
|
|
10539
|
+
uniform float uRipplePatch;
|
|
10540
|
+
uniform float uRippleGain;
|
|
8897
10541
|
/**
|
|
8898
10542
|
* Rocks standing in the current: xz = centre, z = radius, w = how much of the
|
|
8899
10543
|
* water they break (0 = drowned and quiet, 1 = the crest stands proud).
|
|
@@ -8936,6 +10580,32 @@ float riverStreak(vec2 p) {
|
|
|
8936
10580
|
return riverNoise(q) * 0.5 + riverNoise(q * 3.3 + 7.3) * 0.5;
|
|
8937
10581
|
}
|
|
8938
10582
|
|
|
10583
|
+
/** water-that-reads-as-water: the FINE wavelets — the two ripple octaves are
|
|
10584
|
+
* a hand-span and up, so at the range a player actually stands from a creek
|
|
10585
|
+
* the surface tilted in soft blobs and nothing on it could ever face the sun
|
|
10586
|
+
* (the gel read). A third, much shorter field, differenced at its own step
|
|
10587
|
+
* (the ripple's 0.35 step is wider than these wavelets), gives the normal the
|
|
10588
|
+
* facets that glitter and that shatter the reflection; it fades out where a
|
|
10589
|
+
* pixel would integrate it into speckle. Squeezed downstream like the rest. */
|
|
10590
|
+
// Karis' split-sum fit: what a ROUGH surface reflects at this angle — see the
|
|
10591
|
+
// sea-state fresnel note in shaders.ts. A river at a grazing view is a field
|
|
10592
|
+
// of wavelets, not a mirror, and the flat-Schlick sheet of sky it used to wear
|
|
10593
|
+
// is the plastic look this replaces.
|
|
10594
|
+
vec2 envBRDFApprox(float roughness, float NoV) {
|
|
10595
|
+
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
|
|
10596
|
+
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
|
10597
|
+
vec4 r = roughness * c0 + c1;
|
|
10598
|
+
float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
|
|
10599
|
+
return vec2(-1.04, 1.04) * a004 + r.zw;
|
|
10600
|
+
}
|
|
10601
|
+
|
|
10602
|
+
vec2 riverFineSlope(vec2 p) {
|
|
10603
|
+
vec2 q = vec2(p.x * 10.7, p.y * 3.6) + 5.7;
|
|
10604
|
+
const float e = 0.08;
|
|
10605
|
+
float h = riverNoise(q);
|
|
10606
|
+
return vec2(riverNoise(q + vec2(e, 0.0)) - h, riverNoise(q + vec2(0.0, e)) - h) / e;
|
|
10607
|
+
}
|
|
10608
|
+
|
|
8939
10609
|
void main() {
|
|
8940
10610
|
float speed = vRiver.x;
|
|
8941
10611
|
float column = vRiver.y;
|
|
@@ -9009,7 +10679,26 @@ void main() {
|
|
|
9009
10679
|
// the gradient is a NOISE slope, not a water slope: clamp it before it tilts
|
|
9010
10680
|
// the normal past a real wavelet (an over-driven normal throws fresnel to its
|
|
9011
10681
|
// maximum everywhere and the river turns into a sheet of white sky)
|
|
9012
|
-
|
|
10682
|
+
float camDist = length(cameraPosition - vWorldPosition);
|
|
10683
|
+
float fineFade = (1.0 - smoothstep(6.0, 34.0, camDist)) * lively;
|
|
10684
|
+
vec2 fine = vec2(0.0);
|
|
10685
|
+
if (fineFade > 0.01) {
|
|
10686
|
+
vec2 fineA = riverFineSlope(base - vec2(travel * pA, 0.0));
|
|
10687
|
+
vec2 fineB = riverFineSlope(base - vec2(travel * pB, 0.0));
|
|
10688
|
+
// under the ripple FFT the noise keeps only the sub-pixel flash
|
|
10689
|
+
fine = mix(fineA, fineB, blend) *
|
|
10690
|
+
(0.028 * fineFade * (0.5 + 0.5 * uSparkle) * (uRippleOn > 0.5 ? 0.45 : 1.0));
|
|
10691
|
+
}
|
|
10692
|
+
// the ripple FFT, carried downstream in the ribbon's metres with the same
|
|
10693
|
+
// dual-phase crossfade the noise uses (a field scrolled at a speed that
|
|
10694
|
+
// varies along the river smears; two half-period copies do not)
|
|
10695
|
+
if (uRippleOn > 0.5) {
|
|
10696
|
+
float travelM = speed / max(uFlowRate, 0.001);
|
|
10697
|
+
vec2 rA = texture2D(uRippleNormal, (vFlowUv - vec2(travelM * pA, 0.0)) / uRipplePatch, -1.0).xy;
|
|
10698
|
+
vec2 rB = texture2D(uRippleNormal, (vFlowUv - vec2(travelM * pB, 0.0)) / uRipplePatch, -1.0).xy;
|
|
10699
|
+
fine += mix(rA, rB, blend) * (uRippleGain * lively);
|
|
10700
|
+
}
|
|
10701
|
+
vec2 slopeXY = clamp(ripple.yz * uRippleStrength * lively + fine, vec2(-0.5), vec2(0.5));
|
|
9013
10702
|
vec3 normal = normalize(vSurfaceNormal - dir3 * slopeXY.x - perp3 * slopeXY.y);
|
|
9014
10703
|
|
|
9015
10704
|
vec3 viewDir = normalize(cameraPosition - vWorldPosition);
|
|
@@ -9113,31 +10802,53 @@ void main() {
|
|
|
9113
10802
|
// eye is near the water — hold its dark tone well past the horizon line
|
|
9114
10803
|
vec3 sky = mix(uBankColor, uHorizonColor, smoothstep(0.05, 0.34, reflectDir.y));
|
|
9115
10804
|
sky = mix(sky, uSkyColor, smoothstep(0.34, 0.85, reflectDir.y));
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
0.
|
|
9120
|
-
|
|
10805
|
+
if (uUseEnvironmentMap > 0.5) {
|
|
10806
|
+
// the REAL sky, from the river's own cube — near-clipped, so the lowest
|
|
10807
|
+
// rays (which meet the bank a few metres off) keep the bank tone
|
|
10808
|
+
vec3 lifted = vec3(reflectDir.x, max(reflectDir.y, 0.06), reflectDir.z);
|
|
10809
|
+
vec3 cube = min(textureCube(uEnvironmentMap, lifted).rgb, vec3(2.5));
|
|
10810
|
+
sky = mix(uBankColor, cube, smoothstep(0.02, 0.2, reflectDir.y));
|
|
10811
|
+
}
|
|
10812
|
+
float rough = clamp(0.045 + 0.28 * smoothstep(8.0, 140.0, camDist), 0.04, 0.35);
|
|
10813
|
+
float NoVf = clamp(dot(normal, viewDir), 0.0, 1.0);
|
|
10814
|
+
float fresnel = uFresnelScale * (0.02 + 0.98 * pow(1.0 - NoVf, 5.0));
|
|
10815
|
+
{
|
|
10816
|
+
vec2 fRough = envBRDFApprox(rough, NoVf);
|
|
10817
|
+
vec2 fSmooth = envBRDFApprox(0.02, NoVf);
|
|
10818
|
+
fresnel *= clamp((0.02 * fRough.x + fRough.y) / max(0.02 * fSmooth.x + fSmooth.y, 1e-3), 0.0, 1.35);
|
|
10819
|
+
}
|
|
10820
|
+
fresnel = clamp(fresnel, 0.0, 0.82);
|
|
9121
10821
|
|
|
9122
10822
|
vec3 sunDir = normalize(uSunDirection);
|
|
9123
10823
|
vec3 halfVec = normalize(sunDir + viewDir);
|
|
9124
|
-
|
|
9125
|
-
//
|
|
9126
|
-
//
|
|
9127
|
-
//
|
|
9128
|
-
//
|
|
9129
|
-
//
|
|
9130
|
-
//
|
|
9131
|
-
//
|
|
9132
|
-
//
|
|
9133
|
-
//
|
|
9134
|
-
float
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
float
|
|
9138
|
-
|
|
10824
|
+
// ── the sun as a microfacet highlight ──────────────────────────────────
|
|
10825
|
+
// water-that-reads-as-water: the two Blinn-Phong lobes (160 + a whisper of
|
|
10826
|
+
// 34) were a hand-shaped sheen gated by the ripple field, capped at 1.2 —
|
|
10827
|
+
// a running creek under a 40°-high sun showed no sun at all. One GGX lobe
|
|
10828
|
+
// over the wavelet normal: resolved facets near the eye flash (the glitter
|
|
10829
|
+
// a riffle throws), and with distance the roughness grows so the same lobe
|
|
10830
|
+
// widens into the soft sheen a river shows from across the valley. HDR by
|
|
10831
|
+
// design — the sun's reflection is the brightest thing on the water and
|
|
10832
|
+
// the bloom pass reads it. RIVER_SUN_RADIANCE scales the sun against the
|
|
10833
|
+
// unit-ish sky gradient this shader mirrors.
|
|
10834
|
+
const float RIVER_SUN_RADIANCE = 30.0;
|
|
10835
|
+
float NoL = clamp(dot(normal, sunDir), 0.0, 1.0);
|
|
10836
|
+
float NoV = max(dot(normal, viewDir), 1e-3);
|
|
10837
|
+
float NoH = clamp(dot(normal, halfVec), 0.0, 1.0);
|
|
10838
|
+
float LoH = clamp(dot(sunDir, halfVec), 0.0, 1.0);
|
|
10839
|
+
float a2 = rough * rough;
|
|
10840
|
+
float dD = NoH * NoH * (a2 - 1.0) + 1.0;
|
|
10841
|
+
float D = a2 / (3.14159265 * dD * dD);
|
|
10842
|
+
float gV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
|
|
10843
|
+
float gL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
|
|
10844
|
+
float Vis = 0.5 / max(gV + gL, 1e-4);
|
|
10845
|
+
float Fh = 0.02 + 0.98 * pow(1.0 - LoH, 5.0);
|
|
10846
|
+
float spec = D * Vis * Fh * NoL * RIVER_SUN_RADIANCE * uSunIntensity;
|
|
10847
|
+
spec *= 0.5 + 0.5 * uSparkle; // the ripples dial still tunes how much sun the water throws
|
|
9139
10848
|
spec *= 1.0 - foam * 0.85; // froth is rough — it kills the mirror
|
|
9140
|
-
|
|
10849
|
+
// a facet under a pixel aliases into a lone flash: the roughness ramp
|
|
10850
|
+
// widens the lobe out there, the cap catches the last isolated ones
|
|
10851
|
+
spec = min(spec, mix(5.0, 1.2, smoothstep(30.0, 200.0, camDist)));
|
|
9141
10852
|
|
|
9142
10853
|
fresnel *= 1.0 - 0.85 * fall; // froth scatters; it does not reflect
|
|
9143
10854
|
vec3 color = mix(body, sky, fresnel);
|
|
@@ -9237,6 +10948,21 @@ const FOAM_COLOR = "#e9f2ef";
|
|
|
9237
10948
|
/** Per-frame scratch (no allocation in the render path). */
|
|
9238
10949
|
const rockCamScratch = new Vector3();
|
|
9239
10950
|
/** Fixed look constants — the props stay intent-level, these carry the craft. */
|
|
10951
|
+
/** The river's own sky: a slow cube of the world beyond its banks (the sky,
|
|
10952
|
+
* the far hills), refreshed every couple of seconds — what the surface
|
|
10953
|
+
* mirrors instead of a painted three-colour gradient. Near-clipped so the
|
|
10954
|
+
* banks themselves, metres away, never smear across it. */
|
|
10955
|
+
const RIVER_SKY_RESOLUTION = 128;
|
|
10956
|
+
const RIVER_SKY_INTERVAL = 2e3;
|
|
10957
|
+
const RIVER_SKY_NEAR = 60;
|
|
10958
|
+
/** The live ripple layer (see Water3D): a 6 m spectrum patch advected with
|
|
10959
|
+
* the current in the ribbon's own frame. */
|
|
10960
|
+
const RIVER_RIPPLE_SIZE = 128;
|
|
10961
|
+
const RIVER_RIPPLE_PATCH = 6;
|
|
10962
|
+
const RIVER_RIPPLE_FETCH = 150;
|
|
10963
|
+
const RIVER_RIPPLE_SEED = 4242;
|
|
10964
|
+
/** Ripple rms slope per unit of `ripples`. */
|
|
10965
|
+
const RIVER_RIPPLE_SLOPE = .07;
|
|
9240
10966
|
const LOOK = {
|
|
9241
10967
|
/** Pattern units per meter of flow space. */
|
|
9242
10968
|
rippleScale: .85,
|
|
@@ -9379,6 +11105,12 @@ var River3D = class River3D extends Node3D {
|
|
|
9379
11105
|
0
|
|
9380
11106
|
];
|
|
9381
11107
|
_bodies = [];
|
|
11108
|
+
cubeTarget = null;
|
|
11109
|
+
cubeCamera = null;
|
|
11110
|
+
lastSkyAt = 0;
|
|
11111
|
+
rippleFft = null;
|
|
11112
|
+
rippleSpectrum = null;
|
|
11113
|
+
fftUnsupported = false;
|
|
9382
11114
|
/** Loader hook: a malformed river fails at LOAD, not as an invisible ribbon. */
|
|
9383
11115
|
static validateJson(node) {
|
|
9384
11116
|
rejectScale(node, "the flow query and its collider both");
|
|
@@ -9386,7 +11118,7 @@ var River3D = class River3D extends Node3D {
|
|
|
9386
11118
|
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
11119
|
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
11120
|
r.path.forEach((p, i) => {
|
|
9389
|
-
if (!
|
|
11121
|
+
if (!isCoursePoint(p)) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path[${i}] must be ${COURSE_SHAPE}, got ${JSON.stringify(p)}.`, { prop: "path" });
|
|
9390
11122
|
});
|
|
9391
11123
|
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
11124
|
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 +11152,14 @@ var River3D = class River3D extends Node3D {
|
|
|
9420
11152
|
onExitTree() {
|
|
9421
11153
|
findDrapeTerrain(this, this.terrain)?._setExternalChannel(this, null);
|
|
9422
11154
|
}
|
|
11155
|
+
/** Said once when the node has no course to be a river along. */
|
|
11156
|
+
warnedNoPath = false;
|
|
9423
11157
|
update(dt) {
|
|
9424
11158
|
this.time += dt;
|
|
11159
|
+
if (!this.warnedNoPath && this.path.length === 0) {
|
|
11160
|
+
this.warnedNoPath = true;
|
|
11161
|
+
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.`);
|
|
11162
|
+
}
|
|
9425
11163
|
this.ensureCourse();
|
|
9426
11164
|
this.syncSpray();
|
|
9427
11165
|
if (this.flowForce > 0 && this.rings.length > 1) this.sweepBodies(dt);
|
|
@@ -9439,8 +11177,68 @@ var River3D = class River3D extends Node3D {
|
|
|
9439
11177
|
this.rockScanAt = this.time;
|
|
9440
11178
|
this.scanRocks();
|
|
9441
11179
|
}
|
|
9442
|
-
const
|
|
11180
|
+
const mesh = this._ensureObject3D();
|
|
11181
|
+
const u = mesh.material.uniforms;
|
|
9443
11182
|
this.syncRocks(u, ctx.camera.getWorldPosition(rockCamScratch));
|
|
11183
|
+
if (typeof ctx.gl?.extensions?.has === "function") {
|
|
11184
|
+
this.syncSky(ctx, mesh, u);
|
|
11185
|
+
this.syncRipple(ctx, u);
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
/** The sky cube: the world beyond the banks, from above the channel's middle. */
|
|
11189
|
+
syncSky(ctx, mesh, u) {
|
|
11190
|
+
if (!this.cubeTarget) {
|
|
11191
|
+
this.cubeTarget = new WebGLCubeRenderTarget(RIVER_SKY_RESOLUTION, {
|
|
11192
|
+
generateMipmaps: true,
|
|
11193
|
+
minFilter: LinearMipmapLinearFilter,
|
|
11194
|
+
type: HalfFloatType
|
|
11195
|
+
});
|
|
11196
|
+
this.cubeCamera = new CubeCamera(RIVER_SKY_NEAR, 4e3, this.cubeTarget);
|
|
11197
|
+
this.lastSkyAt = 0;
|
|
11198
|
+
}
|
|
11199
|
+
const now = Date.now();
|
|
11200
|
+
if (this.lastSkyAt === 0 || now - this.lastSkyAt >= RIVER_SKY_INTERVAL) {
|
|
11201
|
+
this.lastSkyAt = now;
|
|
11202
|
+
const b = this.ringBounds();
|
|
11203
|
+
const [wx, wy, wz] = this.world;
|
|
11204
|
+
const cam = this.cubeCamera;
|
|
11205
|
+
cam.position.set(wx + (b.minX + b.maxX) / 2, wy + 3, wz + (b.minZ + b.maxZ) / 2);
|
|
11206
|
+
const wasVisible = mesh.visible;
|
|
11207
|
+
mesh.visible = false;
|
|
11208
|
+
cam.update(ctx.gl, ctx.scene);
|
|
11209
|
+
mesh.visible = wasVisible;
|
|
11210
|
+
}
|
|
11211
|
+
if (u.uEnvironmentMap) u.uEnvironmentMap.value = this.cubeTarget.texture;
|
|
11212
|
+
if (u.uUseEnvironmentMap) u.uUseEnvironmentMap.value = 1;
|
|
11213
|
+
}
|
|
11214
|
+
/** The ripple FFT, advected by the fragment in the ribbon's own frame. */
|
|
11215
|
+
syncRipple(ctx, u) {
|
|
11216
|
+
if (this.fftUnsupported) return;
|
|
11217
|
+
if (!this.rippleFft) {
|
|
11218
|
+
if (!seaFftSupported(ctx.gl)) {
|
|
11219
|
+
this.fftUnsupported = true;
|
|
11220
|
+
return;
|
|
11221
|
+
}
|
|
11222
|
+
this.rippleFft = new SeaFft(RIVER_RIPPLE_SIZE, RIVER_RIPPLE_PATCH);
|
|
11223
|
+
this.rippleSpectrum = buildSeaSpectrum({
|
|
11224
|
+
size: RIVER_RIPPLE_SIZE,
|
|
11225
|
+
patch: RIVER_RIPPLE_PATCH,
|
|
11226
|
+
wind: 3,
|
|
11227
|
+
windDirectionDeg: 90,
|
|
11228
|
+
seed: RIVER_RIPPLE_SEED,
|
|
11229
|
+
choppiness: .3,
|
|
11230
|
+
fetch: RIVER_RIPPLE_FETCH,
|
|
11231
|
+
modes: 0
|
|
11232
|
+
});
|
|
11233
|
+
this.rippleFft.setSpectrum(this.rippleSpectrum);
|
|
11234
|
+
}
|
|
11235
|
+
this.rippleFft.update(ctx.gl, this.time);
|
|
11236
|
+
if (u.uRippleNormal) u.uRippleNormal.value = this.rippleFft.normal;
|
|
11237
|
+
if (u.uRippleOn) u.uRippleOn.value = 1;
|
|
11238
|
+
if (u.uRippleGain && this.rippleSpectrum) {
|
|
11239
|
+
const rms = this.rippleSpectrum.rmsSlope;
|
|
11240
|
+
u.uRippleGain.value = rms > 0 ? RIVER_RIPPLE_SLOPE * this.ripples / rms : 0;
|
|
11241
|
+
}
|
|
9444
11242
|
}
|
|
9445
11243
|
/**
|
|
9446
11244
|
* @internal Water surface height over a world point, or null where this
|
|
@@ -9529,6 +11327,11 @@ var River3D = class River3D extends Node3D {
|
|
|
9529
11327
|
const mesh = this._ensureObject3D();
|
|
9530
11328
|
mesh.geometry?.dispose();
|
|
9531
11329
|
mesh.material?.dispose?.();
|
|
11330
|
+
this.cubeTarget?.dispose();
|
|
11331
|
+
this.cubeTarget = null;
|
|
11332
|
+
this.cubeCamera = null;
|
|
11333
|
+
this.rippleFft?.dispose();
|
|
11334
|
+
this.rippleFft = null;
|
|
9532
11335
|
super.free();
|
|
9533
11336
|
}
|
|
9534
11337
|
widthProfile() {
|
|
@@ -9552,7 +11355,7 @@ var River3D = class River3D extends Node3D {
|
|
|
9552
11355
|
const [tx, , tz] = worldXZY(terrain);
|
|
9553
11356
|
const hm = terrain._heightmap();
|
|
9554
11357
|
terrain._setExternalChannel(this, riverCarveChannels({
|
|
9555
|
-
path: this.path.map((p) => [
|
|
11358
|
+
path: course(this.path).map((p) => [p[0] + wx - tx, p[1] + wz - tz]),
|
|
9556
11359
|
widths: this.widthProfile(),
|
|
9557
11360
|
depth: this.depth,
|
|
9558
11361
|
cell: (terrain.size[0] ?? 1) / Math.max(1, hm.segsX),
|
|
@@ -9597,7 +11400,7 @@ var River3D = class River3D extends Node3D {
|
|
|
9597
11400
|
this.fitted = terrain !== null;
|
|
9598
11401
|
const widths = this.widthProfile();
|
|
9599
11402
|
this.rings = buildRiverRings({
|
|
9600
|
-
path: this.path,
|
|
11403
|
+
path: course(this.path),
|
|
9601
11404
|
widths,
|
|
9602
11405
|
depth: this.depth,
|
|
9603
11406
|
flowSpeed: this.flowSpeed,
|
|
@@ -9688,6 +11491,12 @@ var River3D = class River3D extends Node3D {
|
|
|
9688
11491
|
uRockCount: { value: 0 },
|
|
9689
11492
|
uRocks: { value: Array.from({ length: ROCK_SLOTS }, () => new Vector4()) },
|
|
9690
11493
|
uFresnelScale: { value: LOOK.fresnelScale },
|
|
11494
|
+
uEnvironmentMap: { value: null },
|
|
11495
|
+
uUseEnvironmentMap: { value: 0 },
|
|
11496
|
+
uRippleOn: { value: 0 },
|
|
11497
|
+
uRippleNormal: { value: null },
|
|
11498
|
+
uRipplePatch: { value: RIVER_RIPPLE_PATCH },
|
|
11499
|
+
uRippleGain: { value: 0 },
|
|
9691
11500
|
uStandingAmp: { value: LOOK.standingAmp },
|
|
9692
11501
|
uGlideAmp: { value: LOOK.glideAmp }
|
|
9693
11502
|
},
|
|
@@ -11615,6 +13424,8 @@ function paintVerticalGradient(geometry, base, darkest, brightest, rng) {
|
|
|
11615
13424
|
}
|
|
11616
13425
|
//#endregion
|
|
11617
13426
|
//#region src/3d/nodes/tree-3d.ts
|
|
13427
|
+
/** How many times a wet draw is re-rolled before the tree is planted anyway. */
|
|
13428
|
+
const DRY_TRIES = 8;
|
|
11618
13429
|
/** A whole forest in one node is still ≤6 draw calls — but keep it sane. */
|
|
11619
13430
|
const MAX_TREE_COUNT = 500;
|
|
11620
13431
|
/** Whole-grove triangle ceiling: count × tris-per-tree must stay under this. */
|
|
@@ -11770,6 +13581,7 @@ var Tree3D = class Tree3D extends Node3D {
|
|
|
11770
13581
|
default: null,
|
|
11771
13582
|
kinds: ["null", "boolean"]
|
|
11772
13583
|
},
|
|
13584
|
+
avoidWater: { default: true },
|
|
11773
13585
|
terrain: {
|
|
11774
13586
|
default: "",
|
|
11775
13587
|
nodePath: true
|
|
@@ -11809,6 +13621,19 @@ var Tree3D = class Tree3D extends Node3D {
|
|
|
11809
13621
|
* it (keeps retrying until a terrain appears), false disables draping.
|
|
11810
13622
|
*/
|
|
11811
13623
|
drape = null;
|
|
13624
|
+
/**
|
|
13625
|
+
* Keep trunks out of standing and running water (default true) — a valley a
|
|
13626
|
+
* rill runs through has a grove on its banks, not trees in the current.
|
|
13627
|
+
*
|
|
13628
|
+
* `Foliage3D` and `Flowers3D` have had this since they were written, with the
|
|
13629
|
+
* same default and the same reason; the TREE — the tallest thing in a
|
|
13630
|
+
* landscape and the one that most obviously does not grow midstream — did
|
|
13631
|
+
* not. Measured on a river-valley game: two of twenty-two trunks stood in the
|
|
13632
|
+
* rill.
|
|
13633
|
+
*
|
|
13634
|
+
* `false` is the mangrove, the cypress swamp and the flooded forest.
|
|
13635
|
+
*/
|
|
13636
|
+
avoidWater = true;
|
|
11812
13637
|
/** Node path to the Terrain3D to drape onto ('' = auto-find the first one). */
|
|
11813
13638
|
terrain = "";
|
|
11814
13639
|
/** Resolved drape target + the grove's world position, set per rebuild. */
|
|
@@ -12062,13 +13887,45 @@ var Tree3D = class Tree3D extends Node3D {
|
|
|
12062
13887
|
}, void 0, this.noteTextureError);
|
|
12063
13888
|
}
|
|
12064
13889
|
/** Per-instance placement + jitter — ONE rng draw order for both paths. */
|
|
13890
|
+
/**
|
|
13891
|
+
* Where every trunk of this grove stands, in WORLD coordinates.
|
|
13892
|
+
*
|
|
13893
|
+
* `drape` is the prop that puts a grove on the ground it grows from, and
|
|
13894
|
+
* nothing could ask whether it had. `UiMinimap.markers()` and
|
|
13895
|
+
* `HudLayer.focusables()` are the same accessor for the same reason: a
|
|
13896
|
+
* feature whose effect no harness can read is a feature nobody checks.
|
|
13897
|
+
*
|
|
13898
|
+
* Recomputed from the seed rather than read back off the InstancedMesh, so it
|
|
13899
|
+
* answers headless too — where there is no mesh at all.
|
|
13900
|
+
*/
|
|
13901
|
+
trunks() {
|
|
13902
|
+
this.resolveDrape();
|
|
13903
|
+
const out = [];
|
|
13904
|
+
const [wx, wy, wz] = worldXZY(this);
|
|
13905
|
+
this.scatter(new Rng(this.seed), (_i, matrix) => {
|
|
13906
|
+
positionScratch.setFromMatrixPosition(matrix);
|
|
13907
|
+
out.push([
|
|
13908
|
+
positionScratch.x + wx,
|
|
13909
|
+
positionScratch.y + wy,
|
|
13910
|
+
positionScratch.z + wz
|
|
13911
|
+
]);
|
|
13912
|
+
});
|
|
13913
|
+
return out;
|
|
13914
|
+
}
|
|
12065
13915
|
scatter(rng, place) {
|
|
12066
13916
|
const halfX = (this.area[0] ?? 0) / 2;
|
|
12067
13917
|
const halfZ = (this.area[1] ?? 0) / 2;
|
|
13918
|
+
const wet = this.avoidWater ? collectWetSurfaces(this) : [];
|
|
13919
|
+
const [wx, wy, wz] = worldXZY(this);
|
|
12068
13920
|
for (let i = 0; i < this.count; i++) {
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
13921
|
+
let lx = this.count === 1 ? 0 : rng.range(-halfX, halfX);
|
|
13922
|
+
let lz = this.count === 1 ? 0 : rng.range(-halfZ, halfZ);
|
|
13923
|
+
if (wet.length > 0) for (let tries = 0; tries < DRY_TRIES; tries++) {
|
|
13924
|
+
if (!isUnderWater(wet, lx + wx, lz + wz, this.bedY(lx, lz) + wy)) break;
|
|
13925
|
+
lx = rng.range(-halfX, halfX);
|
|
13926
|
+
lz = rng.range(-halfZ, halfZ);
|
|
13927
|
+
}
|
|
13928
|
+
positionScratch.set(lx, this.bedY(lx, lz), lz);
|
|
12072
13929
|
quaternionScratch.setFromAxisAngle(Y_AXIS, rng.range(0, Math.PI * 2));
|
|
12073
13930
|
const h = this.height * rng.range(HEIGHT_JITTER[0], HEIGHT_JITTER[1]);
|
|
12074
13931
|
const girth = h * rng.range(.9, 1.1);
|
|
@@ -12244,6 +14101,195 @@ const quaternionScratch = new Quaternion();
|
|
|
12244
14101
|
const scaleScratch = new Vector3();
|
|
12245
14102
|
const colorScratch = new Color();
|
|
12246
14103
|
//#endregion
|
|
14104
|
+
//#region src/3d/nodes/vehicle-3d.ts
|
|
14105
|
+
const DEG = Math.PI / 180;
|
|
14106
|
+
const RAD2DEG = 180 / Math.PI;
|
|
14107
|
+
/**
|
|
14108
|
+
* A car in one node — Rapier's raycast vehicle under a `RigidBody3D` chassis.
|
|
14109
|
+
*
|
|
14110
|
+
* ```json
|
|
14111
|
+
* { "name": "Car", "type": "RigidBody3D",
|
|
14112
|
+
* "props": { "mass": 800, "collider": { "shape": "box", "size": [1.8, 0.6, 4] } },
|
|
14113
|
+
* "children": [
|
|
14114
|
+
* { "name": "Drive", "type": "Vehicle3D", "props": { "wheels": [
|
|
14115
|
+
* { "position": [-0.8, -0.1, 1.4], "steer": true },
|
|
14116
|
+
* { "position": [ 0.8, -0.1, 1.4], "steer": true },
|
|
14117
|
+
* { "position": [-0.8, -0.1, -1.4], "drive": true },
|
|
14118
|
+
* { "position": [ 0.8, -0.1, -1.4], "drive": true } ] } },
|
|
14119
|
+
* { "name": "Wheel0", "type": "Node3D", "props": { "position": [-0.8, -0.1, 1.4] },
|
|
14120
|
+
* "children": [ { "name": "Hub", "type": "Node3D", "children": [ …the tyre mesh… ] } ] }
|
|
14121
|
+
* ] }
|
|
14122
|
+
* ```
|
|
14123
|
+
*
|
|
14124
|
+
* The wheels are springs that never touch anything (a ray each), so a car
|
|
14125
|
+
* cannot catch a kerb on a wheel body or disagree with its axle; the throttle,
|
|
14126
|
+
* steer and brake come from the `move` vector and the `brake` button (or from
|
|
14127
|
+
* code, with `moveAction: ""`). Forward is +z, the +Z-forward rule. A child
|
|
14128
|
+
* named `Wheel<i>` is posed every step — it hangs on its spring and turns to
|
|
14129
|
+
* steer — and its child `Hub` spins as the wheel rolls.
|
|
14130
|
+
*/
|
|
14131
|
+
var Vehicle3D = class extends Node3D {
|
|
14132
|
+
static typeName = "Vehicle3D";
|
|
14133
|
+
static props = {
|
|
14134
|
+
/** `[{ position, radius?, rest?, steer?, drive? }]` — at least one; defaults radius 0.35, rest 0.3. */
|
|
14135
|
+
wheels: { default: [] },
|
|
14136
|
+
/** Spring stiffness (N/m per kg-ish — 20 soft, 30 a car, 60 a kart). */
|
|
14137
|
+
suspensionStiffness: { default: 30 },
|
|
14138
|
+
suspensionCompression: { default: 2.3 },
|
|
14139
|
+
suspensionRelaxation: { default: 3.5 },
|
|
14140
|
+
/** How far a spring may compress from rest (m). */
|
|
14141
|
+
maxTravel: { default: .3 },
|
|
14142
|
+
/** Tyre grip along the wheel (higher = less spin-out). */
|
|
14143
|
+
friction: { default: 2.5 },
|
|
14144
|
+
/** Tyre grip across the wheel (lower = more drift). */
|
|
14145
|
+
sideFriction: { default: 1 },
|
|
14146
|
+
/** Full lock, degrees. */
|
|
14147
|
+
maxSteerDeg: { default: 32 },
|
|
14148
|
+
/** Force on each driven wheel at full throttle (N). */
|
|
14149
|
+
engineForce: { default: 1500 },
|
|
14150
|
+
/** Brake force on every wheel at full brake. */
|
|
14151
|
+
brakeForce: { default: 40 },
|
|
14152
|
+
/** The stick: up = throttle, down = reverse, left/right = steer. '' = code writes `throttle`/`steer`. */
|
|
14153
|
+
moveAction: { default: "move" },
|
|
14154
|
+
/** Held = full brake. */
|
|
14155
|
+
brakeAction: { default: "brake" },
|
|
14156
|
+
/**
|
|
14157
|
+
* Off = parked: the throttle and steering read nothing and the brake is
|
|
14158
|
+
* held, while the suspension still settles and the wheels still pose.
|
|
14159
|
+
* The switch a game flips while nobody is behind the wheel — both the
|
|
14160
|
+
* car and the character read `move`, so without it one press moves both.
|
|
14161
|
+
*/
|
|
14162
|
+
enabled: { default: true }
|
|
14163
|
+
};
|
|
14164
|
+
wheels = [];
|
|
14165
|
+
suspensionStiffness = 30;
|
|
14166
|
+
suspensionCompression = 2.3;
|
|
14167
|
+
suspensionRelaxation = 3.5;
|
|
14168
|
+
maxTravel = .3;
|
|
14169
|
+
friction = 2.5;
|
|
14170
|
+
sideFriction = 1;
|
|
14171
|
+
maxSteerDeg = 32;
|
|
14172
|
+
engineForce = 1500;
|
|
14173
|
+
brakeForce = 40;
|
|
14174
|
+
moveAction = "move";
|
|
14175
|
+
brakeAction = "brake";
|
|
14176
|
+
enabled = true;
|
|
14177
|
+
/** Live inputs, −1..1 (throttle, steer) and 0..1 (brake) — the actions write these, or your code does. */
|
|
14178
|
+
throttle = 0;
|
|
14179
|
+
steer = 0;
|
|
14180
|
+
brake = 0;
|
|
14181
|
+
/** Forward speed, m/s (negative in reverse). */
|
|
14182
|
+
speed = 0;
|
|
14183
|
+
/** @internal Written by the physics adapter each step. */
|
|
14184
|
+
_states = [];
|
|
14185
|
+
reportedActions = /* @__PURE__ */ new Set();
|
|
14186
|
+
/** What wheel `i` is doing. */
|
|
14187
|
+
wheel(i) {
|
|
14188
|
+
return this._states[i] ?? {
|
|
14189
|
+
contact: false,
|
|
14190
|
+
suspension: this.specs()[i]?.rest ?? .3,
|
|
14191
|
+
rotation: 0,
|
|
14192
|
+
steering: 0
|
|
14193
|
+
};
|
|
14194
|
+
}
|
|
14195
|
+
/** The wheels with every default filled in. */
|
|
14196
|
+
specs() {
|
|
14197
|
+
return this.wheels.map((w) => {
|
|
14198
|
+
const raw = w;
|
|
14199
|
+
return {
|
|
14200
|
+
position: [
|
|
14201
|
+
raw.position?.[0] ?? 0,
|
|
14202
|
+
raw.position?.[1] ?? 0,
|
|
14203
|
+
raw.position?.[2] ?? 0
|
|
14204
|
+
],
|
|
14205
|
+
radius: raw.radius ?? .35,
|
|
14206
|
+
rest: raw.rest ?? .3,
|
|
14207
|
+
steer: raw.steer ?? false,
|
|
14208
|
+
drive: raw.drive ?? false
|
|
14209
|
+
};
|
|
14210
|
+
});
|
|
14211
|
+
}
|
|
14212
|
+
get body() {
|
|
14213
|
+
const parent = this.parent;
|
|
14214
|
+
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"}').`);
|
|
14215
|
+
return parent;
|
|
14216
|
+
}
|
|
14217
|
+
onEnterTree() {
|
|
14218
|
+
this.body;
|
|
14219
|
+
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"? }.`);
|
|
14220
|
+
this.wheels.forEach((w, i) => {
|
|
14221
|
+
const p = w.position;
|
|
14222
|
+
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.`);
|
|
14223
|
+
});
|
|
14224
|
+
}
|
|
14225
|
+
onExitTree() {
|
|
14226
|
+
this.body._physics3d?._freeVehicle(this);
|
|
14227
|
+
}
|
|
14228
|
+
fixedUpdate(dt) {
|
|
14229
|
+
const input = this.tree?.engine?.input;
|
|
14230
|
+
const physics = this.body._physics3d;
|
|
14231
|
+
if (input && this.moveAction !== "") {
|
|
14232
|
+
const v = tolerateUnknownAction({
|
|
14233
|
+
engine: this.tree?.engine,
|
|
14234
|
+
who: this.getPath(),
|
|
14235
|
+
reported: this.reportedActions,
|
|
14236
|
+
consequence: "The car will not move.",
|
|
14237
|
+
authored: this.moveAction !== "move"
|
|
14238
|
+
}, this.moveAction, () => input.getVector(this.moveAction), {
|
|
14239
|
+
x: 0,
|
|
14240
|
+
y: 0
|
|
14241
|
+
});
|
|
14242
|
+
this.throttle = -v.y;
|
|
14243
|
+
this.steer = v.x;
|
|
14244
|
+
this.brake = tolerateUnknownAction({
|
|
14245
|
+
engine: this.tree?.engine,
|
|
14246
|
+
who: this.getPath(),
|
|
14247
|
+
reported: this.reportedActions,
|
|
14248
|
+
consequence: "The car will never brake.",
|
|
14249
|
+
authored: this.brakeAction !== "brake"
|
|
14250
|
+
}, this.brakeAction, () => input.isPressed(this.brakeAction) ? 1 : 0, 0);
|
|
14251
|
+
}
|
|
14252
|
+
if (!this.enabled) {
|
|
14253
|
+
this.throttle = 0;
|
|
14254
|
+
this.steer = 0;
|
|
14255
|
+
this.brake = 1;
|
|
14256
|
+
}
|
|
14257
|
+
if (physics) physics._driveVehicle(this, dt);
|
|
14258
|
+
this.poseWheels();
|
|
14259
|
+
}
|
|
14260
|
+
/** `Wheel<i>` hangs on its spring and turns to steer; its `Hub` spins. */
|
|
14261
|
+
poseWheels() {
|
|
14262
|
+
const specs = this.specs();
|
|
14263
|
+
for (let i = 0; i < specs.length; i++) {
|
|
14264
|
+
const spec = specs[i];
|
|
14265
|
+
if (!spec) continue;
|
|
14266
|
+
const wheel = this.parent?.getNodeOrNull(`Wheel${i}`);
|
|
14267
|
+
if (!wheel || !(wheel instanceof Node3D)) continue;
|
|
14268
|
+
const s = this.wheel(i);
|
|
14269
|
+
wheel.position = [
|
|
14270
|
+
spec.position[0],
|
|
14271
|
+
spec.position[1] - s.suspension,
|
|
14272
|
+
spec.position[2]
|
|
14273
|
+
];
|
|
14274
|
+
wheel.rotation = [
|
|
14275
|
+
0,
|
|
14276
|
+
s.steering * RAD2DEG,
|
|
14277
|
+
0
|
|
14278
|
+
];
|
|
14279
|
+
const hub = wheel.getNodeOrNull("Hub");
|
|
14280
|
+
if (hub && hub instanceof Node3D) hub.rotation = [
|
|
14281
|
+
s.rotation * RAD2DEG,
|
|
14282
|
+
hub.rotation[1] ?? 0,
|
|
14283
|
+
hub.rotation[2] ?? 0
|
|
14284
|
+
];
|
|
14285
|
+
}
|
|
14286
|
+
}
|
|
14287
|
+
/** Full lock, radians. */
|
|
14288
|
+
get maxSteer() {
|
|
14289
|
+
return this.maxSteerDeg * DEG;
|
|
14290
|
+
}
|
|
14291
|
+
};
|
|
14292
|
+
//#endregion
|
|
12247
14293
|
//#region src/3d/nodes/voxel-grid-3d.ts
|
|
12248
14294
|
/**
|
|
12249
14295
|
* Face colors per tile (front,right,back,left,top,bottom) — the agent8
|
|
@@ -13282,6 +15328,213 @@ void main() {
|
|
|
13282
15328
|
}
|
|
13283
15329
|
`;
|
|
13284
15330
|
//#endregion
|
|
15331
|
+
//#region src/3d/nodes/weather-3d.ts
|
|
15332
|
+
/**
|
|
15333
|
+
* Rain falls this fast (m/s); snow this slow. Sizes are in px like every
|
|
15334
|
+
* particle prop (×0.01 = world metres, attenuated with distance): a 3 px drop
|
|
15335
|
+
* is 3 cm, sub-pixel ten metres away and invisible against a bright sky —
|
|
15336
|
+
* measured on the rooftops — so a drop is a hand wide and a flake wider.
|
|
15337
|
+
*/
|
|
15338
|
+
const RAIN = {
|
|
15339
|
+
speed: [10, 14],
|
|
15340
|
+
size: 55,
|
|
15341
|
+
color: "#eaf3ff",
|
|
15342
|
+
alpha: .75,
|
|
15343
|
+
rate: 1500,
|
|
15344
|
+
spread: 4,
|
|
15345
|
+
cap: 4e3
|
|
15346
|
+
};
|
|
15347
|
+
const SNOW = {
|
|
15348
|
+
speed: [.6, 1.4],
|
|
15349
|
+
size: 14,
|
|
15350
|
+
color: "#ffffff",
|
|
15351
|
+
alpha: .9,
|
|
15352
|
+
rate: 500,
|
|
15353
|
+
spread: 50,
|
|
15354
|
+
cap: 6e3
|
|
15355
|
+
};
|
|
15356
|
+
const M_TO_PX = 100;
|
|
15357
|
+
/** How often the tree is re-scanned for the current camera (fixed steps). */
|
|
15358
|
+
const CAMERA_SCAN_STEPS = 60;
|
|
15359
|
+
/**
|
|
15360
|
+
* Weather — rain or snow that follows the camera.
|
|
15361
|
+
*
|
|
15362
|
+
* The scene format had a sky, a fog, a cloud deck and a sun, and no weather:
|
|
15363
|
+
* rain was forty lines of `Particles3D` tuning an author had to know, plus a
|
|
15364
|
+
* script to keep the emitter over the camera. One node now:
|
|
15365
|
+
*
|
|
15366
|
+
* ```json
|
|
15367
|
+
* { "name": "Rain", "type": "Weather3D", "props": { "kind": "rain", "intensity": 0.7, "wind": [2, 0] } }
|
|
15368
|
+
* ```
|
|
15369
|
+
*
|
|
15370
|
+
* The drops are born anywhere in a box `radius` wide and `height` above
|
|
15371
|
+
* whatever is followed (the current `Camera3D` unless `follow` names a node),
|
|
15372
|
+
* fall in world space, and stay where they fell when the camera moves on.
|
|
15373
|
+
* Every `Particles3D` prop is still here to override (a colour, a size), but
|
|
15374
|
+
* `kind` sets them all and `intensity` scales the rate live.
|
|
15375
|
+
*/
|
|
15376
|
+
/**
|
|
15377
|
+
* A raindrop's stamp: a vertical streak with soft ends, not a dot.
|
|
15378
|
+
*
|
|
15379
|
+
* Rain is drawn as point sprites, and a point sprite is SQUARE — so the streak
|
|
15380
|
+
* lives inside the texture: a thin bright column down the middle with
|
|
15381
|
+
* transparent sides. Measured before this existed: a downpour at intensity 1
|
|
15382
|
+
* put 2,600 round dots on screen and read as a dirty lens.
|
|
15383
|
+
*/
|
|
15384
|
+
let streakStamp = null;
|
|
15385
|
+
function streakSprite() {
|
|
15386
|
+
if (streakStamp) return streakStamp;
|
|
15387
|
+
const size = 64;
|
|
15388
|
+
const data = new Uint8Array(size * size * 4);
|
|
15389
|
+
const halfW = size * .055;
|
|
15390
|
+
const centre = (size - 1) / 2;
|
|
15391
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
15392
|
+
const dx = Math.abs(x - centre) / halfW;
|
|
15393
|
+
const along = Math.abs(y - centre) / centre;
|
|
15394
|
+
const ends = Math.max(0, 1 - Math.max(0, along - .72) / .28);
|
|
15395
|
+
const across = Math.max(0, 1 - dx * dx);
|
|
15396
|
+
const i = (y * size + x) * 4;
|
|
15397
|
+
data[i] = 255;
|
|
15398
|
+
data[i + 1] = 255;
|
|
15399
|
+
data[i + 2] = 255;
|
|
15400
|
+
data[i + 3] = Math.round(Math.min(1, across * ends) * 255);
|
|
15401
|
+
}
|
|
15402
|
+
const texture = new DataTexture(data, size, size, RGBAFormat);
|
|
15403
|
+
texture.needsUpdate = true;
|
|
15404
|
+
streakStamp = texture;
|
|
15405
|
+
return texture;
|
|
15406
|
+
}
|
|
15407
|
+
var Weather3D = class extends Particles3D {
|
|
15408
|
+
static typeName = "Weather3D";
|
|
15409
|
+
static props = {
|
|
15410
|
+
/** `rain` — fast, straight, a shower; `snow` — slow, drifting flakes. */
|
|
15411
|
+
kind: {
|
|
15412
|
+
default: "rain",
|
|
15413
|
+
options: ["rain", "snow"]
|
|
15414
|
+
},
|
|
15415
|
+
/** How much of it, 0..1 (scales the rate live; 0 = a dry sky). */
|
|
15416
|
+
intensity: {
|
|
15417
|
+
default: .6,
|
|
15418
|
+
range: {
|
|
15419
|
+
min: 0,
|
|
15420
|
+
max: 1
|
|
15421
|
+
}
|
|
15422
|
+
},
|
|
15423
|
+
/** Half-width (m) of the box the drops are born in, around the camera. */
|
|
15424
|
+
radius: { default: 20 },
|
|
15425
|
+
/** How far above the camera they are born (m). */
|
|
15426
|
+
height: { default: 12 },
|
|
15427
|
+
/** A steady drift, m/s, on the ground plane `[x, z]`. */
|
|
15428
|
+
wind: { default: [0, 0] },
|
|
15429
|
+
/** What to follow; '' = the current Camera3D. */
|
|
15430
|
+
follow: {
|
|
15431
|
+
default: "",
|
|
15432
|
+
nodePath: true
|
|
15433
|
+
}
|
|
15434
|
+
};
|
|
15435
|
+
kind = "rain";
|
|
15436
|
+
intensity = .6;
|
|
15437
|
+
radius = 20;
|
|
15438
|
+
height = 12;
|
|
15439
|
+
wind = [0, 0];
|
|
15440
|
+
follow = "";
|
|
15441
|
+
/** Rain is a streak; snow keeps the soft flake the particle system draws. */
|
|
15442
|
+
sprite() {
|
|
15443
|
+
return this.kind === "snow" ? super.sprite() : streakSprite();
|
|
15444
|
+
}
|
|
15445
|
+
tunedFor = "";
|
|
15446
|
+
camera = null;
|
|
15447
|
+
cameraScanIn = 0;
|
|
15448
|
+
onEnterTree() {
|
|
15449
|
+
super.onEnterTree?.();
|
|
15450
|
+
this.tune();
|
|
15451
|
+
}
|
|
15452
|
+
/** Set the inherited particle props from `kind` — once, and again if it changes. */
|
|
15453
|
+
tune() {
|
|
15454
|
+
const t = this.kind === "snow" ? SNOW : RAIN;
|
|
15455
|
+
this.worldSpace = true;
|
|
15456
|
+
this.blend = "normal";
|
|
15457
|
+
this.directionDeg = 90;
|
|
15458
|
+
this.spreadDeg = t.spread;
|
|
15459
|
+
this.speed = [(t.speed[0] ?? 1) * M_TO_PX, (t.speed[1] ?? 1) * M_TO_PX];
|
|
15460
|
+
const fall = t.speed[0] ?? 1;
|
|
15461
|
+
this.lifetime = [(this.height + 2) / fall, (this.height + 6) / fall];
|
|
15462
|
+
this.gravity = [
|
|
15463
|
+
0,
|
|
15464
|
+
0,
|
|
15465
|
+
0
|
|
15466
|
+
];
|
|
15467
|
+
this.drag = 0;
|
|
15468
|
+
this.sizeStart = t.size;
|
|
15469
|
+
this.sizeEnd = t.size;
|
|
15470
|
+
this.colorStart = t.color;
|
|
15471
|
+
this.colorEnd = t.color;
|
|
15472
|
+
this.alphaStart = t.alpha;
|
|
15473
|
+
this.alphaEnd = t.alpha * .8;
|
|
15474
|
+
this.maxParticles = t.cap;
|
|
15475
|
+
this.emitBox = [
|
|
15476
|
+
this.radius,
|
|
15477
|
+
.5,
|
|
15478
|
+
this.radius
|
|
15479
|
+
];
|
|
15480
|
+
this.drift = [
|
|
15481
|
+
(this.wind[0] ?? 0) * M_TO_PX,
|
|
15482
|
+
0,
|
|
15483
|
+
(this.wind[1] ?? 0) * M_TO_PX
|
|
15484
|
+
];
|
|
15485
|
+
this.rate = t.rate * this.intensity;
|
|
15486
|
+
this.tunedFor = this.tuning();
|
|
15487
|
+
this._rebuildSim();
|
|
15488
|
+
}
|
|
15489
|
+
tuning() {
|
|
15490
|
+
return `${this.kind}/${this.radius}/${this.height}/${this.wind[0] ?? 0}/${this.wind[1] ?? 0}`;
|
|
15491
|
+
}
|
|
15492
|
+
followed() {
|
|
15493
|
+
if (this.follow !== "") return this.getNodeOrNull(this.follow);
|
|
15494
|
+
if (this.cameraScanIn <= 0 || !this.camera || this.camera.tree === null) {
|
|
15495
|
+
this.camera = currentCamera(this.getRoot());
|
|
15496
|
+
this.cameraScanIn = CAMERA_SCAN_STEPS;
|
|
15497
|
+
}
|
|
15498
|
+
this.cameraScanIn -= 1;
|
|
15499
|
+
return this.camera;
|
|
15500
|
+
}
|
|
15501
|
+
update(dt) {
|
|
15502
|
+
if (this.tunedFor !== this.tuning()) this.tune();
|
|
15503
|
+
const t = this.kind === "snow" ? SNOW : RAIN;
|
|
15504
|
+
this.rate = t.rate * Math.max(0, Math.min(1, this.intensity));
|
|
15505
|
+
const target = this.followed();
|
|
15506
|
+
if (target) {
|
|
15507
|
+
const at = worldPosition(target);
|
|
15508
|
+
const parentAt = this.parent ? worldPosition(this.parent) : [
|
|
15509
|
+
0,
|
|
15510
|
+
0,
|
|
15511
|
+
0
|
|
15512
|
+
];
|
|
15513
|
+
this.position = [
|
|
15514
|
+
(at[0] ?? 0) - (parentAt[0] ?? 0),
|
|
15515
|
+
(at[1] ?? 0) + this.height - (parentAt[1] ?? 0),
|
|
15516
|
+
(at[2] ?? 0) - (parentAt[2] ?? 0)
|
|
15517
|
+
];
|
|
15518
|
+
}
|
|
15519
|
+
super.update(dt);
|
|
15520
|
+
}
|
|
15521
|
+
};
|
|
15522
|
+
function currentCamera(root) {
|
|
15523
|
+
let first = null;
|
|
15524
|
+
const walk = (n) => {
|
|
15525
|
+
if (n instanceof Camera3D) {
|
|
15526
|
+
if (n.current) return n;
|
|
15527
|
+
first ??= n;
|
|
15528
|
+
}
|
|
15529
|
+
for (const c of n.children) {
|
|
15530
|
+
const hit = walk(c);
|
|
15531
|
+
if (hit) return hit;
|
|
15532
|
+
}
|
|
15533
|
+
return null;
|
|
15534
|
+
};
|
|
15535
|
+
return walk(root) ?? first;
|
|
15536
|
+
}
|
|
15537
|
+
//#endregion
|
|
13285
15538
|
//#region src/3d/register.ts
|
|
13286
15539
|
/**
|
|
13287
15540
|
* Register the 3D node taxonomy (and the core nodes). Call once in your game
|
|
@@ -13291,6 +15544,10 @@ function registerNodes3D() {
|
|
|
13291
15544
|
registerCoreNodes();
|
|
13292
15545
|
registerNode(Node3D);
|
|
13293
15546
|
registerNode(Particles3D);
|
|
15547
|
+
registerNode(Weather3D);
|
|
15548
|
+
registerNode(Flock3D);
|
|
15549
|
+
registerNode(Ragdoll3D);
|
|
15550
|
+
registerNode(Vehicle3D);
|
|
13294
15551
|
registerNode(MeshInstance3D);
|
|
13295
15552
|
registerNode(LoftMesh3D);
|
|
13296
15553
|
registerNode(Sprite3D);
|
|
@@ -13353,4 +15610,4 @@ function resolveEnvironmentHdri(env) {
|
|
|
13353
15610
|
return null;
|
|
13354
15611
|
}
|
|
13355
15612
|
//#endregion
|
|
13356
|
-
export {
|
|
15613
|
+
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 };
|