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