@jgengine/core 0.5.0 → 0.7.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/CHANGELOG.md +191 -2
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +5 -0
- package/dist/game/events.d.ts +62 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +291 -5
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +133 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +53 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +219 -0
- package/dist/physics/physicsWorld.js +749 -0
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +138 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +66 -2
- package/dist/runtime/gameContext.js +157 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/scene/assetCatalog.d.ts +13 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +7 -1
- package/dist/scene/entityStore.js +15 -1
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +62 -0
- package/dist/time/simClock.js +132 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/buildingIndex.d.ts +16 -0
- package/dist/world/buildingIndex.js +63 -0
- package/dist/world/buildings.d.ts +98 -0
- package/dist/world/buildings.js +320 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/features.d.ts +105 -1
- package/dist/world/features.js +95 -0
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +48 -0
- package/dist/world/placementController.js +97 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/regions.d.ts +66 -0
- package/dist/world/regions.js +158 -0
- package/dist/world/scatter.d.ts +24 -0
- package/dist/world/scatter.js +126 -0
- package/dist/world/scatterItems.d.ts +38 -0
- package/dist/world/scatterItems.js +85 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +54 -0
- package/dist/world/terrain.js +181 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/water.d.ts +27 -0
- package/dist/world/water.js +125 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/dist/world/wind.d.ts +17 -0
- package/dist/world/wind.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PhysicsWorld } from "./physicsWorld.js";
|
|
2
|
+
export interface RagdollBoneSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
position: readonly [number, number, number];
|
|
5
|
+
halfExtents: readonly [number, number, number];
|
|
6
|
+
/** Default 1. */
|
|
7
|
+
mass?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RagdollLinkSpec {
|
|
10
|
+
a: string;
|
|
11
|
+
b: string;
|
|
12
|
+
/** `hinge` pins the shared anchor; `distance` (default) holds the bones' current separation. */
|
|
13
|
+
kind?: "hinge" | "distance";
|
|
14
|
+
/** World point for a hinge pin. Defaults to the midpoint of the two bone centers. */
|
|
15
|
+
anchor?: readonly [number, number, number];
|
|
16
|
+
}
|
|
17
|
+
export interface RagdollBalanceConfig {
|
|
18
|
+
/** Bone the motor keeps upright (torso/head). */
|
|
19
|
+
root: string;
|
|
20
|
+
/** Target height of the root above the floor the motor drives toward. */
|
|
21
|
+
targetHeight: number;
|
|
22
|
+
/** Corrective gain pulling the root back to `targetHeight`. Default 8. */
|
|
23
|
+
strength?: number;
|
|
24
|
+
/** Locomotion push applied to the root from the move vector (active-ragdoll walking). Default 6. */
|
|
25
|
+
moveForce?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RagdollConfig {
|
|
28
|
+
bones: RagdollBoneSpec[];
|
|
29
|
+
links: RagdollLinkSpec[];
|
|
30
|
+
/** Optional active-ragdoll balance motor; omit for a purely floppy ragdoll. */
|
|
31
|
+
balance?: RagdollBalanceConfig;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A jointed multi-body character — floppy by default, or active-ragdoll when a balance motor is
|
|
35
|
+
* configured. Bones are `PhysicsWorld` bodies; links are joints on the joint API. `balance(dt, mx, mz)`
|
|
36
|
+
* drives the root toward its target height (staying upright) and pushes it by the move vector.
|
|
37
|
+
*/
|
|
38
|
+
export declare class Ragdoll {
|
|
39
|
+
readonly bones: Record<string, number>;
|
|
40
|
+
readonly links: number[];
|
|
41
|
+
private readonly world;
|
|
42
|
+
private readonly config?;
|
|
43
|
+
constructor(world: PhysicsWorld, bones: Record<string, number>, links: number[], balance?: RagdollBalanceConfig);
|
|
44
|
+
centerOfMass(): [number, number, number];
|
|
45
|
+
applyImpulse(bone: string, x: number, y: number, z: number): void;
|
|
46
|
+
balance(dt: number, moveX?: number, moveZ?: number): void;
|
|
47
|
+
remove(): void;
|
|
48
|
+
}
|
|
49
|
+
export declare function createRagdoll(world: PhysicsWorld, config: RagdollConfig): Ragdoll;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A jointed multi-body character — floppy by default, or active-ragdoll when a balance motor is
|
|
3
|
+
* configured. Bones are `PhysicsWorld` bodies; links are joints on the joint API. `balance(dt, mx, mz)`
|
|
4
|
+
* drives the root toward its target height (staying upright) and pushes it by the move vector.
|
|
5
|
+
*/
|
|
6
|
+
export class Ragdoll {
|
|
7
|
+
bones;
|
|
8
|
+
links;
|
|
9
|
+
world;
|
|
10
|
+
config;
|
|
11
|
+
constructor(world, bones, links, balance) {
|
|
12
|
+
this.world = world;
|
|
13
|
+
this.bones = bones;
|
|
14
|
+
this.links = links;
|
|
15
|
+
this.config = balance;
|
|
16
|
+
}
|
|
17
|
+
centerOfMass() {
|
|
18
|
+
const w = this.world;
|
|
19
|
+
let mx = 0;
|
|
20
|
+
let my = 0;
|
|
21
|
+
let mz = 0;
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const id of Object.values(this.bones)) {
|
|
24
|
+
const im = w.invMass[id];
|
|
25
|
+
const m = im > 0 ? 1 / im : 0;
|
|
26
|
+
mx += w.posX[id] * m;
|
|
27
|
+
my += w.posY[id] * m;
|
|
28
|
+
mz += w.posZ[id] * m;
|
|
29
|
+
total += m;
|
|
30
|
+
}
|
|
31
|
+
if (total === 0)
|
|
32
|
+
return [0, 0, 0];
|
|
33
|
+
return [mx / total, my / total, mz / total];
|
|
34
|
+
}
|
|
35
|
+
applyImpulse(bone, x, y, z) {
|
|
36
|
+
const w = this.world;
|
|
37
|
+
const id = this.bones[bone];
|
|
38
|
+
if (id === undefined)
|
|
39
|
+
return;
|
|
40
|
+
const im = w.invMass[id];
|
|
41
|
+
w.velX[id] += x * im;
|
|
42
|
+
w.velY[id] += y * im;
|
|
43
|
+
w.velZ[id] += z * im;
|
|
44
|
+
w.wake(id);
|
|
45
|
+
}
|
|
46
|
+
balance(dt, moveX = 0, moveZ = 0) {
|
|
47
|
+
const c = this.config;
|
|
48
|
+
if (c === undefined)
|
|
49
|
+
return;
|
|
50
|
+
const w = this.world;
|
|
51
|
+
const id = this.bones[c.root];
|
|
52
|
+
if (id === undefined)
|
|
53
|
+
return;
|
|
54
|
+
const strength = c.strength ?? 8;
|
|
55
|
+
const moveForce = c.moveForce ?? 6;
|
|
56
|
+
const err = c.targetHeight - w.posY[id];
|
|
57
|
+
w.velY[id] += err * strength * dt;
|
|
58
|
+
w.velX[id] += moveX * moveForce * dt;
|
|
59
|
+
w.velZ[id] += moveZ * moveForce * dt;
|
|
60
|
+
w.wake(id);
|
|
61
|
+
}
|
|
62
|
+
remove() {
|
|
63
|
+
for (const j of this.links)
|
|
64
|
+
this.world.removeJoint(j);
|
|
65
|
+
this.links.length = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function createRagdoll(world, config) {
|
|
69
|
+
const bones = {};
|
|
70
|
+
for (const spec of config.bones) {
|
|
71
|
+
bones[spec.name] = world.addBody({
|
|
72
|
+
position: spec.position,
|
|
73
|
+
halfExtents: spec.halfExtents,
|
|
74
|
+
mass: spec.mass ?? 1,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const links = [];
|
|
78
|
+
for (const link of config.links) {
|
|
79
|
+
const a = bones[link.a];
|
|
80
|
+
const b = bones[link.b];
|
|
81
|
+
if (a === undefined || b === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
if ((link.kind ?? "distance") === "hinge") {
|
|
84
|
+
const mid = link.anchor !== undefined
|
|
85
|
+
? [link.anchor[0], link.anchor[1], link.anchor[2]]
|
|
86
|
+
: [
|
|
87
|
+
(world.posX[a] + world.posX[b]) / 2,
|
|
88
|
+
(world.posY[a] + world.posY[b]) / 2,
|
|
89
|
+
(world.posZ[a] + world.posZ[b]) / 2,
|
|
90
|
+
];
|
|
91
|
+
links.push(world.hingeJoint({
|
|
92
|
+
bodyA: a,
|
|
93
|
+
bodyB: b,
|
|
94
|
+
anchorA: [mid[0] - world.posX[a], mid[1] - world.posY[a], mid[2] - world.posZ[a]],
|
|
95
|
+
anchorB: [mid[0] - world.posX[b], mid[1] - world.posY[b], mid[2] - world.posZ[b]],
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
links.push(world.distanceJoint({ bodyA: a, bodyB: b }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return new Ragdoll(world, bones, links, config.balance);
|
|
103
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PhysicsBounds } from "./physicsWorld.js";
|
|
2
|
+
export interface SpatialGridConfig {
|
|
3
|
+
/** Horizontal extent to index; only x/z of the bounds are used (a top-down plane). */
|
|
4
|
+
bounds: PhysicsBounds;
|
|
5
|
+
/** Cell edge; tune to ≈ the query/contact radius so a hit touches ≤ 4 cells. */
|
|
6
|
+
cellSize: number;
|
|
7
|
+
/** Maximum entities the index holds; fixed at construction. */
|
|
8
|
+
capacity: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap
|
|
12
|
+
* same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each
|
|
13
|
+
* tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an
|
|
14
|
+
* AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false
|
|
15
|
+
* positives beyond the exact distance test.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SpatialGrid {
|
|
18
|
+
readonly cellSize: number;
|
|
19
|
+
readonly nx: number;
|
|
20
|
+
readonly nz: number;
|
|
21
|
+
readonly capacity: number;
|
|
22
|
+
private readonly minX;
|
|
23
|
+
private readonly minZ;
|
|
24
|
+
private readonly numCells;
|
|
25
|
+
private readonly cellStart;
|
|
26
|
+
private readonly cursor;
|
|
27
|
+
private readonly sorted;
|
|
28
|
+
private readonly cellOfBody;
|
|
29
|
+
private xs;
|
|
30
|
+
private zs;
|
|
31
|
+
private entityCount;
|
|
32
|
+
constructor(config: SpatialGridConfig);
|
|
33
|
+
get count(): number;
|
|
34
|
+
private cellX;
|
|
35
|
+
private cellZ;
|
|
36
|
+
rebuild(count: number, xs: Float32Array, zs: Float32Array): void;
|
|
37
|
+
/** Fill `out` with every entity within `radius` of (x,z); returns the hit count (capped at `out.length`). */
|
|
38
|
+
queryCircle(x: number, z: number, radius: number, out: Int32Array): number;
|
|
39
|
+
/** Invoke `cb(a, b)` once for each unordered pair within `maxDistance` (mutual separation/overlap). */
|
|
40
|
+
forEachPair(maxDistance: number, cb: (a: number, b: number) => void): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap
|
|
3
|
+
* same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each
|
|
4
|
+
* tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an
|
|
5
|
+
* AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false
|
|
6
|
+
* positives beyond the exact distance test.
|
|
7
|
+
*/
|
|
8
|
+
export class SpatialGrid {
|
|
9
|
+
cellSize;
|
|
10
|
+
nx;
|
|
11
|
+
nz;
|
|
12
|
+
capacity;
|
|
13
|
+
minX;
|
|
14
|
+
minZ;
|
|
15
|
+
numCells;
|
|
16
|
+
cellStart;
|
|
17
|
+
cursor;
|
|
18
|
+
sorted;
|
|
19
|
+
cellOfBody;
|
|
20
|
+
xs = null;
|
|
21
|
+
zs = null;
|
|
22
|
+
entityCount = 0;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.cellSize = config.cellSize;
|
|
25
|
+
this.capacity = config.capacity;
|
|
26
|
+
this.minX = config.bounds.min[0];
|
|
27
|
+
this.minZ = config.bounds.min[2];
|
|
28
|
+
const spanX = Math.max(config.bounds.max[0] - config.bounds.min[0], config.cellSize);
|
|
29
|
+
const spanZ = Math.max(config.bounds.max[2] - config.bounds.min[2], config.cellSize);
|
|
30
|
+
this.nx = Math.max(1, Math.ceil(spanX / config.cellSize));
|
|
31
|
+
this.nz = Math.max(1, Math.ceil(spanZ / config.cellSize));
|
|
32
|
+
this.numCells = this.nx * this.nz;
|
|
33
|
+
this.cellStart = new Int32Array(this.numCells + 1);
|
|
34
|
+
this.cursor = new Int32Array(this.numCells);
|
|
35
|
+
this.sorted = new Int32Array(config.capacity);
|
|
36
|
+
this.cellOfBody = new Int32Array(config.capacity);
|
|
37
|
+
}
|
|
38
|
+
get count() {
|
|
39
|
+
return this.entityCount;
|
|
40
|
+
}
|
|
41
|
+
cellX(x) {
|
|
42
|
+
const raw = Math.floor((x - this.minX) / this.cellSize);
|
|
43
|
+
return raw < 0 ? 0 : raw >= this.nx ? this.nx - 1 : raw;
|
|
44
|
+
}
|
|
45
|
+
cellZ(z) {
|
|
46
|
+
const raw = Math.floor((z - this.minZ) / this.cellSize);
|
|
47
|
+
return raw < 0 ? 0 : raw >= this.nz ? this.nz - 1 : raw;
|
|
48
|
+
}
|
|
49
|
+
rebuild(count, xs, zs) {
|
|
50
|
+
this.entityCount = count;
|
|
51
|
+
this.xs = xs;
|
|
52
|
+
this.zs = zs;
|
|
53
|
+
const start = this.cellStart;
|
|
54
|
+
start.fill(0);
|
|
55
|
+
for (let i = 0; i < count; i += 1) {
|
|
56
|
+
const c = this.cellZ(zs[i]) * this.nx + this.cellX(xs[i]);
|
|
57
|
+
this.cellOfBody[i] = c;
|
|
58
|
+
start[c + 1] += 1;
|
|
59
|
+
}
|
|
60
|
+
for (let c = 0; c < this.numCells; c += 1) {
|
|
61
|
+
start[c + 1] += start[c];
|
|
62
|
+
this.cursor[c] = start[c];
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < count; i += 1) {
|
|
65
|
+
const c = this.cellOfBody[i];
|
|
66
|
+
this.sorted[this.cursor[c]++] = i;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Fill `out` with every entity within `radius` of (x,z); returns the hit count (capped at `out.length`). */
|
|
70
|
+
queryCircle(x, z, radius, out) {
|
|
71
|
+
const xs = this.xs;
|
|
72
|
+
const zs = this.zs;
|
|
73
|
+
if (xs === null || zs === null)
|
|
74
|
+
return 0;
|
|
75
|
+
const r2 = radius * radius;
|
|
76
|
+
const cxMin = this.cellX(x - radius);
|
|
77
|
+
const cxMax = this.cellX(x + radius);
|
|
78
|
+
const czMin = this.cellZ(z - radius);
|
|
79
|
+
const czMax = this.cellZ(z + radius);
|
|
80
|
+
const limit = out.length;
|
|
81
|
+
let n = 0;
|
|
82
|
+
for (let cz = czMin; cz <= czMax; cz += 1) {
|
|
83
|
+
const rowBase = cz * this.nx;
|
|
84
|
+
for (let cx = cxMin; cx <= cxMax; cx += 1) {
|
|
85
|
+
const cell = rowBase + cx;
|
|
86
|
+
const end = this.cellStart[cell + 1];
|
|
87
|
+
for (let s = this.cellStart[cell]; s < end; s += 1) {
|
|
88
|
+
const id = this.sorted[s];
|
|
89
|
+
const dx = xs[id] - x;
|
|
90
|
+
const dz = zs[id] - z;
|
|
91
|
+
if (dx * dx + dz * dz <= r2) {
|
|
92
|
+
if (n >= limit)
|
|
93
|
+
return n;
|
|
94
|
+
out[n++] = id;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return n;
|
|
100
|
+
}
|
|
101
|
+
/** Invoke `cb(a, b)` once for each unordered pair within `maxDistance` (mutual separation/overlap). */
|
|
102
|
+
forEachPair(maxDistance, cb) {
|
|
103
|
+
const xs = this.xs;
|
|
104
|
+
const zs = this.zs;
|
|
105
|
+
if (xs === null || zs === null)
|
|
106
|
+
return;
|
|
107
|
+
const d2 = maxDistance * maxDistance;
|
|
108
|
+
const nx = this.nx;
|
|
109
|
+
const nz = this.nz;
|
|
110
|
+
for (let i = 0; i < this.entityCount; i += 1) {
|
|
111
|
+
const c = this.cellOfBody[i];
|
|
112
|
+
const cz = (c / nx) | 0;
|
|
113
|
+
const cx = c - cz * nx;
|
|
114
|
+
const x = xs[i];
|
|
115
|
+
const z = zs[i];
|
|
116
|
+
const z0 = cz > 0 ? cz - 1 : cz;
|
|
117
|
+
const z1 = cz < nz - 1 ? cz + 1 : cz;
|
|
118
|
+
const x0 = cx > 0 ? cx - 1 : cx;
|
|
119
|
+
const x1 = cx < nx - 1 ? cx + 1 : cx;
|
|
120
|
+
for (let gz = z0; gz <= z1; gz += 1) {
|
|
121
|
+
const rowBase = gz * nx;
|
|
122
|
+
for (let gx = x0; gx <= x1; gx += 1) {
|
|
123
|
+
const cell = rowBase + gx;
|
|
124
|
+
const end = this.cellStart[cell + 1];
|
|
125
|
+
for (let s = this.cellStart[cell]; s < end; s += 1) {
|
|
126
|
+
const j = this.sorted[s];
|
|
127
|
+
if (j <= i)
|
|
128
|
+
continue;
|
|
129
|
+
const dx = xs[j] - x;
|
|
130
|
+
const dz = zs[j] - z;
|
|
131
|
+
if (dx * dx + dz * dz <= d2)
|
|
132
|
+
cb(i, j);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { PhysicsWorld } from "./physicsWorld.js";
|
|
2
|
+
export interface StructureMaterial {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Node integrity and edge strength default to this — brittle glass vs load-bearing concrete. */
|
|
5
|
+
strength: number;
|
|
6
|
+
/** Debris body mass when a node of this material collapses. Default 1. */
|
|
7
|
+
mass?: number;
|
|
8
|
+
}
|
|
9
|
+
export type StructureMaterialTable = Readonly<Record<string, StructureMaterial>>;
|
|
10
|
+
export interface StructureNodeSpec {
|
|
11
|
+
id: string;
|
|
12
|
+
/** Body centre in world coordinates — also where its debris spawns. */
|
|
13
|
+
position: readonly [number, number, number];
|
|
14
|
+
halfExtents: readonly [number, number, number];
|
|
15
|
+
/** Overrides material mass for the collapsed debris body. */
|
|
16
|
+
mass?: number;
|
|
17
|
+
/** Hit points before the piece shatters. Default from `material.strength`, else 100. */
|
|
18
|
+
integrity?: number;
|
|
19
|
+
material?: string;
|
|
20
|
+
/** Foundation piece — reachability to any anchor is what keeps the rest standing. */
|
|
21
|
+
anchor?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface StructureEdgeSpec {
|
|
24
|
+
a: string;
|
|
25
|
+
b: string;
|
|
26
|
+
/** Accumulated edge damage past this severs the connection. Default from `material.strength`, else 100. */
|
|
27
|
+
strength?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CollapseEvent {
|
|
30
|
+
/**
|
|
31
|
+
* Node ids that left the structure this operation — the shattered node plus every piece the loss
|
|
32
|
+
* disconnected from an anchor. This is the replicated collapse: broadcast the id list, not each
|
|
33
|
+
* fragment's physics (games re-derive the debris locally).
|
|
34
|
+
*/
|
|
35
|
+
fell: readonly string[];
|
|
36
|
+
collapsed: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface DebrisConfig {
|
|
39
|
+
/** Outward kick magnitude applied to each debris body from `origin`. Default 3. */
|
|
40
|
+
impulse?: number;
|
|
41
|
+
/** Blast origin the kick radiates from. Defaults to each node's own position (straight drop). */
|
|
42
|
+
origin?: readonly [number, number, number];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A structural-integrity graph over a building — nodes are pieces (walls, beams, floors), edges are
|
|
46
|
+
* load-bearing connections, some nodes are anchored foundations. `damage`/`damageEdge` wear pieces and
|
|
47
|
+
* connections down; when a piece shatters or an edge severs, the graph recomputes which pieces still
|
|
48
|
+
* reach an anchor and hands back every newly-disconnected piece as one `CollapseEvent`. Feed that to
|
|
49
|
+
* `toDebris` to sink the fallen pieces into a `PhysicsWorld` as rigid bodies ("The Finals" smooth
|
|
50
|
+
* destruction, Rainbow Six walls). Coarse by design: it replicates the collapse event, not per fragment.
|
|
51
|
+
*/
|
|
52
|
+
export declare class StructureGraph {
|
|
53
|
+
private readonly nodes;
|
|
54
|
+
private readonly integrity;
|
|
55
|
+
private readonly adj;
|
|
56
|
+
private readonly edgeStrength;
|
|
57
|
+
private readonly edgeDamage;
|
|
58
|
+
private readonly fallenSpecs;
|
|
59
|
+
private readonly materials;
|
|
60
|
+
constructor(config: {
|
|
61
|
+
nodes: readonly StructureNodeSpec[];
|
|
62
|
+
edges: readonly StructureEdgeSpec[];
|
|
63
|
+
materials?: StructureMaterialTable;
|
|
64
|
+
});
|
|
65
|
+
private materialStrength;
|
|
66
|
+
has(id: string): boolean;
|
|
67
|
+
integrityOf(id: string): number;
|
|
68
|
+
nodeSpec(id: string): StructureNodeSpec | undefined;
|
|
69
|
+
neighbors(id: string): string[];
|
|
70
|
+
standing(): string[];
|
|
71
|
+
isSupported(id: string): boolean;
|
|
72
|
+
private supportedSet;
|
|
73
|
+
private removeNode;
|
|
74
|
+
private settle;
|
|
75
|
+
/** Wear a piece down; at ≤0 integrity it shatters and the loss cascades through unsupported pieces. */
|
|
76
|
+
damage(id: string, amount: number): CollapseEvent;
|
|
77
|
+
/** Wear a connection down; past its strength it severs and unsupported pieces cascade off. */
|
|
78
|
+
damageEdge(a: string, b: string, amount: number): CollapseEvent;
|
|
79
|
+
/** Cut a connection outright (a beam sheared by a blast), then cascade. */
|
|
80
|
+
severEdge(a: string, b: string): CollapseEvent;
|
|
81
|
+
/** Spawn one rigid debris body per fallen piece into `world`; returns the new body indices. */
|
|
82
|
+
toDebris(world: PhysicsWorld, event: CollapseEvent, config?: DebrisConfig): number[];
|
|
83
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PhysicsWorld } from "./physicsWorld.js";
|
|
2
|
+
export interface GrappleConfig {
|
|
3
|
+
/** Metres per second the rope shortens under `reel` / lengthens under `payOut`. Default 8. */
|
|
4
|
+
reelSpeed?: number;
|
|
5
|
+
/** Rope refuses to shorten past this — the traveller stops on top of the anchor, not inside it. Default 1.5. */
|
|
6
|
+
minLength?: number;
|
|
7
|
+
/** Firing range; `fire` beyond this misses. Default 40. */
|
|
8
|
+
maxLength?: number;
|
|
9
|
+
/** Elastic rope (spring) that yanks and bounces, vs a rigid `distance` cable that holds a hard length. Default false. */
|
|
10
|
+
elastic?: boolean;
|
|
11
|
+
/** Spring restoring rate when `elastic`. Default 40. */
|
|
12
|
+
stiffness?: number;
|
|
13
|
+
/** Spring damping when `elastic`. Default 6. */
|
|
14
|
+
damping?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A fired-anchor rope on the joint API — grapple (reel toward a hit point), zipline (rigid cable to a
|
|
18
|
+
* far anchor you then slide/reel along), swing (rigid rope + gravity = a pendulum). `fire` attaches a
|
|
19
|
+
* `distance`/`spring` joint from the traveller body to a fixed world point; `reel` shrinks its rest
|
|
20
|
+
* length so the constraint drags the body in; `moveAnchor` re-points it (zipline glide, grapple-to-
|
|
21
|
+
* moving-target). The pick — a raycast to find the anchor — is the caller's; core owns the constraint.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Grapple {
|
|
24
|
+
readonly body: number;
|
|
25
|
+
private readonly world;
|
|
26
|
+
private readonly reelSpeed;
|
|
27
|
+
private readonly minLength;
|
|
28
|
+
private readonly maxLength;
|
|
29
|
+
private readonly elastic;
|
|
30
|
+
private readonly stiffness;
|
|
31
|
+
private readonly damping;
|
|
32
|
+
private joint;
|
|
33
|
+
private length;
|
|
34
|
+
private ax;
|
|
35
|
+
private ay;
|
|
36
|
+
private az;
|
|
37
|
+
constructor(world: PhysicsWorld, body: number, config?: GrappleConfig);
|
|
38
|
+
get attached(): boolean;
|
|
39
|
+
get ropeLength(): number;
|
|
40
|
+
anchor(): readonly [number, number, number] | null;
|
|
41
|
+
distanceToAnchor(): number;
|
|
42
|
+
/** Attach to a world anchor point. Returns false (a miss) when the point is beyond `maxLength`. */
|
|
43
|
+
fire(x: number, y: number, z: number): boolean;
|
|
44
|
+
/** Shorten the rope by `reelSpeed * dt`, pulling the traveller toward the anchor. Clamps at `minLength`. */
|
|
45
|
+
reel(dt: number): void;
|
|
46
|
+
/** Pay out slack, lengthening the rope up to `maxLength`. */
|
|
47
|
+
payOut(dt: number): void;
|
|
48
|
+
/** Re-point the anchor (slide a zipline attachment along the cable, or track a moving target). */
|
|
49
|
+
moveAnchor(x: number, y: number, z: number): void;
|
|
50
|
+
release(): void;
|
|
51
|
+
}
|
|
52
|
+
export interface GlideConfig {
|
|
53
|
+
/** Fraction of world gravity the glider still feels — 1 is normal fall, 0 hangs. Default 0.25. */
|
|
54
|
+
gravityScale?: number;
|
|
55
|
+
/** Forward acceleration applied along the steer vector each `apply`. Default 6. */
|
|
56
|
+
thrust?: number;
|
|
57
|
+
/** Terminal descent clamp so a glide never plummets. Default 8. */
|
|
58
|
+
maxFallSpeed?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A reduced-gravity, forward-thrust glide over a physics body — wingsuit / glider / paraglider
|
|
62
|
+
* (Enshrouded, Grounded). Call `apply(dt, steerX, steerZ)` each frame *before* `world.step`: it feeds
|
|
63
|
+
* back most of the gravity the sim is about to apply (leaving `gravityScale` of it), pushes the body
|
|
64
|
+
* along the steer vector by `thrust`, and clamps descent to `maxFallSpeed`. Stop calling it to fall
|
|
65
|
+
* normally again — no attach/detach state to leak.
|
|
66
|
+
*/
|
|
67
|
+
export declare class Glide {
|
|
68
|
+
readonly body: number;
|
|
69
|
+
private readonly world;
|
|
70
|
+
private readonly gravityScale;
|
|
71
|
+
private readonly thrust;
|
|
72
|
+
private readonly maxFallSpeed;
|
|
73
|
+
constructor(world: PhysicsWorld, body: number, config?: GlideConfig);
|
|
74
|
+
apply(dt: number, steerX?: number, steerZ?: number): void;
|
|
75
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fired-anchor rope on the joint API — grapple (reel toward a hit point), zipline (rigid cable to a
|
|
3
|
+
* far anchor you then slide/reel along), swing (rigid rope + gravity = a pendulum). `fire` attaches a
|
|
4
|
+
* `distance`/`spring` joint from the traveller body to a fixed world point; `reel` shrinks its rest
|
|
5
|
+
* length so the constraint drags the body in; `moveAnchor` re-points it (zipline glide, grapple-to-
|
|
6
|
+
* moving-target). The pick — a raycast to find the anchor — is the caller's; core owns the constraint.
|
|
7
|
+
*/
|
|
8
|
+
export class Grapple {
|
|
9
|
+
body;
|
|
10
|
+
world;
|
|
11
|
+
reelSpeed;
|
|
12
|
+
minLength;
|
|
13
|
+
maxLength;
|
|
14
|
+
elastic;
|
|
15
|
+
stiffness;
|
|
16
|
+
damping;
|
|
17
|
+
joint = -1;
|
|
18
|
+
length = 0;
|
|
19
|
+
ax = 0;
|
|
20
|
+
ay = 0;
|
|
21
|
+
az = 0;
|
|
22
|
+
constructor(world, body, config = {}) {
|
|
23
|
+
this.world = world;
|
|
24
|
+
this.body = body;
|
|
25
|
+
this.reelSpeed = config.reelSpeed ?? 8;
|
|
26
|
+
this.minLength = config.minLength ?? 1.5;
|
|
27
|
+
this.maxLength = config.maxLength ?? 40;
|
|
28
|
+
this.elastic = config.elastic ?? false;
|
|
29
|
+
this.stiffness = config.stiffness ?? 40;
|
|
30
|
+
this.damping = config.damping ?? 6;
|
|
31
|
+
}
|
|
32
|
+
get attached() {
|
|
33
|
+
return this.joint >= 0;
|
|
34
|
+
}
|
|
35
|
+
get ropeLength() {
|
|
36
|
+
return this.length;
|
|
37
|
+
}
|
|
38
|
+
anchor() {
|
|
39
|
+
return this.joint < 0 ? null : [this.ax, this.ay, this.az];
|
|
40
|
+
}
|
|
41
|
+
distanceToAnchor() {
|
|
42
|
+
const w = this.world;
|
|
43
|
+
const dx = this.ax - w.posX[this.body];
|
|
44
|
+
const dy = this.ay - w.posY[this.body];
|
|
45
|
+
const dz = this.az - w.posZ[this.body];
|
|
46
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
47
|
+
}
|
|
48
|
+
/** Attach to a world anchor point. Returns false (a miss) when the point is beyond `maxLength`. */
|
|
49
|
+
fire(x, y, z) {
|
|
50
|
+
if (this.joint >= 0)
|
|
51
|
+
this.release();
|
|
52
|
+
const w = this.world;
|
|
53
|
+
const dx = x - w.posX[this.body];
|
|
54
|
+
const dy = y - w.posY[this.body];
|
|
55
|
+
const dz = z - w.posZ[this.body];
|
|
56
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
57
|
+
if (dist > this.maxLength)
|
|
58
|
+
return false;
|
|
59
|
+
this.ax = x;
|
|
60
|
+
this.ay = y;
|
|
61
|
+
this.az = z;
|
|
62
|
+
this.length = dist;
|
|
63
|
+
const opts = {
|
|
64
|
+
bodyA: this.body,
|
|
65
|
+
anchorB: [x, y, z],
|
|
66
|
+
restLength: dist,
|
|
67
|
+
stiffness: this.stiffness,
|
|
68
|
+
damping: this.damping,
|
|
69
|
+
};
|
|
70
|
+
this.joint = this.elastic ? w.springJoint(opts) : w.distanceJoint(opts);
|
|
71
|
+
w.wake(this.body);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/** Shorten the rope by `reelSpeed * dt`, pulling the traveller toward the anchor. Clamps at `minLength`. */
|
|
75
|
+
reel(dt) {
|
|
76
|
+
if (this.joint < 0)
|
|
77
|
+
return;
|
|
78
|
+
this.length = Math.max(this.minLength, this.length - this.reelSpeed * dt);
|
|
79
|
+
this.world.setJointRest(this.joint, this.length);
|
|
80
|
+
this.world.wake(this.body);
|
|
81
|
+
}
|
|
82
|
+
/** Pay out slack, lengthening the rope up to `maxLength`. */
|
|
83
|
+
payOut(dt) {
|
|
84
|
+
if (this.joint < 0)
|
|
85
|
+
return;
|
|
86
|
+
this.length = Math.min(this.maxLength, this.length + this.reelSpeed * dt);
|
|
87
|
+
this.world.setJointRest(this.joint, this.length);
|
|
88
|
+
this.world.wake(this.body);
|
|
89
|
+
}
|
|
90
|
+
/** Re-point the anchor (slide a zipline attachment along the cable, or track a moving target). */
|
|
91
|
+
moveAnchor(x, y, z) {
|
|
92
|
+
if (this.joint < 0)
|
|
93
|
+
return;
|
|
94
|
+
this.ax = x;
|
|
95
|
+
this.ay = y;
|
|
96
|
+
this.az = z;
|
|
97
|
+
this.world.setJointAnchor(this.joint, x, y, z);
|
|
98
|
+
this.world.wake(this.body);
|
|
99
|
+
}
|
|
100
|
+
release() {
|
|
101
|
+
if (this.joint < 0)
|
|
102
|
+
return;
|
|
103
|
+
this.world.removeJoint(this.joint);
|
|
104
|
+
this.joint = -1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A reduced-gravity, forward-thrust glide over a physics body — wingsuit / glider / paraglider
|
|
109
|
+
* (Enshrouded, Grounded). Call `apply(dt, steerX, steerZ)` each frame *before* `world.step`: it feeds
|
|
110
|
+
* back most of the gravity the sim is about to apply (leaving `gravityScale` of it), pushes the body
|
|
111
|
+
* along the steer vector by `thrust`, and clamps descent to `maxFallSpeed`. Stop calling it to fall
|
|
112
|
+
* normally again — no attach/detach state to leak.
|
|
113
|
+
*/
|
|
114
|
+
export class Glide {
|
|
115
|
+
body;
|
|
116
|
+
world;
|
|
117
|
+
gravityScale;
|
|
118
|
+
thrust;
|
|
119
|
+
maxFallSpeed;
|
|
120
|
+
constructor(world, body, config = {}) {
|
|
121
|
+
this.world = world;
|
|
122
|
+
this.body = body;
|
|
123
|
+
this.gravityScale = config.gravityScale ?? 0.25;
|
|
124
|
+
this.thrust = config.thrust ?? 6;
|
|
125
|
+
this.maxFallSpeed = config.maxFallSpeed ?? 8;
|
|
126
|
+
}
|
|
127
|
+
apply(dt, steerX = 0, steerZ = 0) {
|
|
128
|
+
const w = this.world;
|
|
129
|
+
const b = this.body;
|
|
130
|
+
const counter = -w.gravity * (1 - this.gravityScale);
|
|
131
|
+
w.velY[b] += counter * dt;
|
|
132
|
+
w.velX[b] += steerX * this.thrust * dt;
|
|
133
|
+
w.velZ[b] += steerZ * this.thrust * dt;
|
|
134
|
+
if (w.velY[b] < -this.maxFallSpeed)
|
|
135
|
+
w.velY[b] = -this.maxFallSpeed;
|
|
136
|
+
w.wake(b);
|
|
137
|
+
}
|
|
138
|
+
}
|