@jgengine/core 0.18.1 → 0.19.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 +158 -7
- package/dist/ai/decisionGraph.d.ts +48 -4
- package/dist/ai/decisionGraph.js +153 -23
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +11 -0
- package/dist/anim/animGraph.js +128 -0
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/locomotionGraph.d.ts +9 -0
- package/dist/anim/locomotionGraph.js +29 -0
- package/dist/audio/audioFalloff.d.ts +2 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/editor/document.js +3 -1
- package/dist/editor/environment.js +8 -0
- package/dist/editor/types.d.ts +4 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +51 -1
- package/dist/game/events.d.ts +6 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/playableGame.d.ts +84 -17
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +36 -2
- package/dist/input/gamepadModel.js +69 -30
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +112 -1
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +21 -2
- package/dist/movement/playerMovement.js +89 -6
- package/dist/movement/solidObstacles.d.ts +9 -1
- package/dist/movement/solidObstacles.js +44 -2
- package/dist/movement/voxelController.js +7 -7
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +32 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +33 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +7 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/inputSnapshot.d.ts +6 -0
- package/dist/runtime/inputSnapshot.js +10 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/scene/behaviorRuntime.d.ts +12 -2
- package/dist/scene/behaviorRuntime.js +30 -14
- package/dist/scene/behaviors.d.ts +5 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +11 -1
- package/dist/sensor/perception.js +57 -10
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +2 -0
- package/dist/settings/graphicsProfile.js +12 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/features.d.ts +29 -2
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +2 -2
- package/dist/world.js +1 -1
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/api.md +462 -436
- package/skills/jgengine-gameplay/capabilities.md +24 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { steerYaw } from "../movement/steering.js";
|
|
2
2
|
import { DEFAULT_GRIP_CURVE, sampleGripCurve } from "./vehicleBody.js";
|
|
3
|
+
/**
|
|
4
|
+
* Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
|
|
5
|
+
*
|
|
6
|
+
* @capability kinematic-vehicle arcade car/kart driving physics with throttle, brake, steer and retunable handling
|
|
7
|
+
*/
|
|
3
8
|
export function createKinematicVehicle(initialTuning, options = {}) {
|
|
4
9
|
// Mutable so `retune` can swap the whole block without disturbing the integrator's state; every
|
|
5
10
|
// read below is per-tick rather than captured at construction for the same reason.
|
|
@@ -309,6 +314,16 @@ export function createKinematicVehicle(initialTuning, options = {}) {
|
|
|
309
314
|
retune(next) {
|
|
310
315
|
tuning = next;
|
|
311
316
|
},
|
|
317
|
+
snapshot: () => ({
|
|
318
|
+
x, y, z, heading, vx, vz, gearIndex, rpm, shiftRemaining, steerAngle, yawRate,
|
|
319
|
+
previousForwardSpeed, bodyPitch, bodyRoll, airOffset, verticalVelocity,
|
|
320
|
+
}),
|
|
321
|
+
restore(state) {
|
|
322
|
+
({
|
|
323
|
+
x, y, z, heading, vx, vz, gearIndex, rpm, shiftRemaining, steerAngle, yawRate,
|
|
324
|
+
previousForwardSpeed, bodyPitch, bodyRoll, airOffset, verticalVelocity,
|
|
325
|
+
} = state);
|
|
326
|
+
},
|
|
312
327
|
resetTo(position, nextHeading) {
|
|
313
328
|
x = position[0];
|
|
314
329
|
y = position[1];
|
|
@@ -52,6 +52,10 @@ export interface BodyDesc {
|
|
|
52
52
|
mask?: number;
|
|
53
53
|
/** Continuous collision detection for fast small bodies (bullets, thrown props). */
|
|
54
54
|
ccd?: boolean;
|
|
55
|
+
/** Per-second linear velocity damping on this body (air drag, water); default `0`. */
|
|
56
|
+
linearDamping?: number;
|
|
57
|
+
/** Per-second angular velocity damping on this body; default `0`. Ignored by rotation-free backends. */
|
|
58
|
+
angularDamping?: number;
|
|
55
59
|
asleep?: boolean;
|
|
56
60
|
userData?: unknown;
|
|
57
61
|
}
|
|
@@ -170,6 +174,14 @@ export interface PhysicsBackend {
|
|
|
170
174
|
setVelocity(handle: BodyHandle, velocity: PhysicsVec3): void;
|
|
171
175
|
setAngularVelocity(handle: BodyHandle, angularVelocity: PhysicsVec3): void;
|
|
172
176
|
applyImpulse(handle: BodyHandle, impulse: PhysicsVec3, point?: PhysicsVec3): void;
|
|
177
|
+
/**
|
|
178
|
+
* Apply a world-space force, N, over the next {@link step} only (thrust, wind, buoyancy, a tow rope). Call it
|
|
179
|
+
* every tick the force should act. A `point` off the centre of mass also applies torque on rotation backends.
|
|
180
|
+
* Pending forces are not part of a snapshot.
|
|
181
|
+
*/
|
|
182
|
+
applyForce(handle: BodyHandle, force: PhysicsVec3, point?: PhysicsVec3): void;
|
|
183
|
+
/** Apply a world-space torque, N·m, over the next {@link step} only; a no-op on rotation-free backends. */
|
|
184
|
+
applyTorque(handle: BodyHandle, torque: PhysicsVec3): void;
|
|
173
185
|
/** Move a kinematic body so the solver sees its velocity over the next step (platforms, doors, hands). */
|
|
174
186
|
setKinematicTarget(handle: BodyHandle, position: PhysicsVec3, rotation?: PhysicsQuat): void;
|
|
175
187
|
wake(handle: BodyHandle): void;
|
|
@@ -109,6 +109,40 @@ export function runPhysicsBackendConformance(create, harness) {
|
|
|
109
109
|
const later = backend.addBody({ shape: { kind: "sphere", radius: 0.3 }, position: [2, 2, 2] });
|
|
110
110
|
expect(later).not.toBe(crate);
|
|
111
111
|
});
|
|
112
|
+
test("applyForce accelerates at F/m for one step and then stops acting", () => {
|
|
113
|
+
const backend = create();
|
|
114
|
+
const held = backend.addBody({ shape: { kind: "sphere", radius: 0.5 }, position: [0, 20, 0], mass: 2 });
|
|
115
|
+
const once = backend.addBody({ shape: { kind: "sphere", radius: 0.5 }, position: [10, 20, 0], mass: 2 });
|
|
116
|
+
backend.applyForce(once, [4, 0, 0]);
|
|
117
|
+
for (let i = 0; i < 60; i += 1) {
|
|
118
|
+
backend.applyForce(held, [4, 0, 0]);
|
|
119
|
+
backend.step(1 / 60);
|
|
120
|
+
}
|
|
121
|
+
expect(backend.body(held).velocity[0]).toBeGreaterThan(1.6);
|
|
122
|
+
expect(backend.body(held).velocity[0]).toBeLessThan(2.2);
|
|
123
|
+
expect(backend.body(once).velocity[0]).toBeGreaterThan(0.02);
|
|
124
|
+
expect(backend.body(once).velocity[0]).toBeLessThan(0.05);
|
|
125
|
+
});
|
|
126
|
+
test("linear damping bleeds a body's speed faster than an undamped twin", () => {
|
|
127
|
+
const backend = create();
|
|
128
|
+
const damped = backend.addBody({ shape: { kind: "sphere", radius: 0.5 }, position: [0, 30, 0], velocity: [10, 0, 0], linearDamping: 1.5 });
|
|
129
|
+
const free = backend.addBody({ shape: { kind: "sphere", radius: 0.5 }, position: [0, 30, 20], velocity: [10, 0, 0] });
|
|
130
|
+
settle(backend, 1);
|
|
131
|
+
expect(backend.body(damped).velocity[0]).toBeLessThan(backend.body(free).velocity[0] * 0.5);
|
|
132
|
+
});
|
|
133
|
+
test("applyTorque spins a body on rotation backends and is harmless elsewhere", () => {
|
|
134
|
+
const backend = create();
|
|
135
|
+
const wheel = backend.addBody({ shape: { kind: "box", halfExtents: [0.5, 0.5, 0.5] }, position: [0, 30, 0] });
|
|
136
|
+
for (let i = 0; i < 30; i += 1) {
|
|
137
|
+
backend.applyTorque(wheel, [0, 2, 0]);
|
|
138
|
+
backend.step(1 / 60);
|
|
139
|
+
}
|
|
140
|
+
const spin = backend.body(wheel).angularVelocity[1];
|
|
141
|
+
if (backend.capabilities.rotation)
|
|
142
|
+
expect(spin).toBeGreaterThan(0.1);
|
|
143
|
+
else
|
|
144
|
+
expect(spin).toBe(0);
|
|
145
|
+
});
|
|
112
146
|
test("capabilities describe the backend", () => {
|
|
113
147
|
const backend = create();
|
|
114
148
|
expect(backend.capabilities.shapes.length).toBeGreaterThan(1);
|
|
@@ -21,6 +21,7 @@ interface SavedBody {
|
|
|
21
21
|
position: [number, number, number];
|
|
22
22
|
velocity: [number, number, number];
|
|
23
23
|
sleeping: boolean;
|
|
24
|
+
linearDamping?: number;
|
|
24
25
|
}
|
|
25
26
|
/** Serializable backend state: every live body and joint, with handles preserved. */
|
|
26
27
|
export interface PhysicsWorldBackendState {
|
|
@@ -132,6 +132,8 @@ export function createPhysicsWorldBackend(options) {
|
|
|
132
132
|
half,
|
|
133
133
|
kinematicVelocity: [0, 0, 0],
|
|
134
134
|
kinematicTarget: null,
|
|
135
|
+
linearDamping: desc.linearDamping ?? 0,
|
|
136
|
+
force: [0, 0, 0],
|
|
135
137
|
});
|
|
136
138
|
handleOfIndex.set(index, handle);
|
|
137
139
|
masses.set(handle, desc.mass ?? 1);
|
|
@@ -248,6 +250,17 @@ export function createPhysicsWorldBackend(options) {
|
|
|
248
250
|
const invMass = world.invMass[i];
|
|
249
251
|
world.setVelocity(i, world.velX[i] + impulse[0] * invMass, world.velY[i] + impulse[1] * invMass, world.velZ[i] + impulse[2] * invMass);
|
|
250
252
|
},
|
|
253
|
+
applyForce(handle, force) {
|
|
254
|
+
const rec = record(handle);
|
|
255
|
+
if (rec.kind !== "dynamic")
|
|
256
|
+
return;
|
|
257
|
+
rec.force[0] += force[0];
|
|
258
|
+
rec.force[1] += force[1];
|
|
259
|
+
rec.force[2] += force[2];
|
|
260
|
+
},
|
|
261
|
+
applyTorque(handle) {
|
|
262
|
+
record(handle);
|
|
263
|
+
},
|
|
251
264
|
setKinematicTarget(handle, position) {
|
|
252
265
|
const rec = record(handle);
|
|
253
266
|
rec.kinematicTarget = [position[0], position[1], position[2]];
|
|
@@ -288,6 +301,24 @@ export function createPhysicsWorldBackend(options) {
|
|
|
288
301
|
world.setPosition(i, target[0], target[1], target[2]);
|
|
289
302
|
rec.kinematicTarget = null;
|
|
290
303
|
}
|
|
304
|
+
for (const rec of records.values()) {
|
|
305
|
+
if (rec.kind !== "dynamic")
|
|
306
|
+
continue;
|
|
307
|
+
const f = rec.force;
|
|
308
|
+
const i = rec.index;
|
|
309
|
+
if (f[0] !== 0 || f[1] !== 0 || f[2] !== 0) {
|
|
310
|
+
const scale = world.invMass[i] * dt;
|
|
311
|
+
world.setVelocity(i, world.velX[i] + f[0] * scale, world.velY[i] + f[1] * scale, world.velZ[i] + f[2] * scale);
|
|
312
|
+
world.wake(i);
|
|
313
|
+
f[0] = 0;
|
|
314
|
+
f[1] = 0;
|
|
315
|
+
f[2] = 0;
|
|
316
|
+
}
|
|
317
|
+
if (rec.linearDamping > 0) {
|
|
318
|
+
const keep = Math.max(0, 1 - rec.linearDamping * dt);
|
|
319
|
+
world.setVelocity(i, world.velX[i] * keep, world.velY[i] * keep, world.velZ[i] * keep);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
291
322
|
world.step(dt);
|
|
292
323
|
},
|
|
293
324
|
raycast(desc) {
|
|
@@ -437,6 +468,7 @@ export function createPhysicsWorldBackend(options) {
|
|
|
437
468
|
position: [world.posX[i], world.posY[i], world.posZ[i]],
|
|
438
469
|
velocity: [world.velX[i], world.velY[i], world.velZ[i]],
|
|
439
470
|
sleeping: world.isSleeping(i),
|
|
471
|
+
...(rec.linearDamping > 0 ? { linearDamping: rec.linearDamping } : {}),
|
|
440
472
|
});
|
|
441
473
|
}
|
|
442
474
|
return {
|
|
@@ -465,6 +497,7 @@ export function createPhysicsWorldBackend(options) {
|
|
|
465
497
|
layers: body.layers,
|
|
466
498
|
mask: body.mask,
|
|
467
499
|
userData: body.userData,
|
|
500
|
+
...(body.linearDamping === undefined ? {} : { linearDamping: body.linearDamping }),
|
|
468
501
|
}, body.sleeping);
|
|
469
502
|
}
|
|
470
503
|
for (const joint of saved.joints) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BodyHandle, PhysicsBackend, PhysicsVec3 } from "./physicsBackend.js";
|
|
2
|
+
/** Options for {@link createVehicleBackendLink}. */
|
|
3
|
+
export interface VehicleBackendLinkOptions {
|
|
4
|
+
/** Chassis collision box half extents, m (width/2, height/2, length/2), heading along +z. */
|
|
5
|
+
halfExtents: PhysicsVec3;
|
|
6
|
+
/** Gap kept between the box bottom and the car's base height, so the cast never scrapes the ground (default `0.1`). */
|
|
7
|
+
clearance?: number;
|
|
8
|
+
/** Collision layer bitmask of the chassis body (default `1`). */
|
|
9
|
+
layers?: number;
|
|
10
|
+
/** Layers the chassis body collides with and shoves in the solver (default all). */
|
|
11
|
+
mask?: number;
|
|
12
|
+
/** Layers that stop the car in `clampMove` (default all). Leave props off it so the car shoves them instead of stopping. */
|
|
13
|
+
blockMask?: number;
|
|
14
|
+
/** Contact normals steeper than this `y` count as ground, not walls, and never block (default `0.7`). */
|
|
15
|
+
groundNormalY?: number;
|
|
16
|
+
}
|
|
17
|
+
/** The pose fields {@link VehicleBackendLink.sync} reads; any vehicle step (`VehicleDynamicsStep`, `KinematicVehicleStep`) fits. */
|
|
18
|
+
export interface VehicleLinkPose {
|
|
19
|
+
position: readonly [number, number, number];
|
|
20
|
+
heading: number;
|
|
21
|
+
bodyPitch?: number;
|
|
22
|
+
bodyRoll?: number;
|
|
23
|
+
}
|
|
24
|
+
/** A wall the last clamped move ran into. */
|
|
25
|
+
export interface VehicleLinkHit {
|
|
26
|
+
body: BodyHandle;
|
|
27
|
+
normal: PhysicsVec3;
|
|
28
|
+
/** Metres of this move the wall absorbed; divide by `dt` for the impact speed. */
|
|
29
|
+
blocked: number;
|
|
30
|
+
}
|
|
31
|
+
/** A vehicle sim's presence in a physics backend: a kinematic chassis that shoves props, and a wall-aware move clamp. */
|
|
32
|
+
export interface VehicleBackendLink {
|
|
33
|
+
readonly body: BodyHandle;
|
|
34
|
+
/** Pass as the sim's `clampMove` option: sweeps the chassis box and stops at walls, sliding along them. */
|
|
35
|
+
clampMove(from: readonly [number, number], to: readonly [number, number]): readonly [number, number];
|
|
36
|
+
/** Move the kinematic chassis to the sim's pose; call after each sim tick, before the backend steps. */
|
|
37
|
+
sync(pose: VehicleLinkPose): void;
|
|
38
|
+
/** The wall the most recent `clampMove` hit, or `null`. */
|
|
39
|
+
lastHit(): VehicleLinkHit | null;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Puts a vehicle sim (`createVehicleDynamics`, `createKinematicVehicle`) into a {@link PhysicsBackend} world without
|
|
44
|
+
* handing its handling to the rigid-body solver: the sim still owns grip and balance, while a kinematic chassis box
|
|
45
|
+
* follows it so Rapier or `PhysicsWorld` props get shoved, and `clampMove` sweeps that box so walls and parked cars
|
|
46
|
+
* stop it and it slides along them.
|
|
47
|
+
* @capability vehicle-backend-link collide a vehicle sim with a physics backend — shove props, stop at walls, slide along them
|
|
48
|
+
*/
|
|
49
|
+
export declare function createVehicleBackendLink(backend: PhysicsBackend, options: VehicleBackendLinkOptions): VehicleBackendLink;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Yaw about +y (heading), then pitch about the car's x, then roll about its z, as a quaternion. */
|
|
2
|
+
function poseQuat(heading, pitch, roll) {
|
|
3
|
+
const hy = heading / 2;
|
|
4
|
+
const hp = pitch / 2;
|
|
5
|
+
const hr = roll / 2;
|
|
6
|
+
const cy = Math.cos(hy);
|
|
7
|
+
const sy = Math.sin(hy);
|
|
8
|
+
const cp = Math.cos(hp);
|
|
9
|
+
const sp = Math.sin(hp);
|
|
10
|
+
const cr = Math.cos(hr);
|
|
11
|
+
const sr = Math.sin(hr);
|
|
12
|
+
return [
|
|
13
|
+
cy * sp * cr + sy * cp * sr,
|
|
14
|
+
sy * cp * cr - cy * sp * sr,
|
|
15
|
+
cy * cp * sr - sy * sp * cr,
|
|
16
|
+
cy * cp * cr + sy * sp * sr,
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Puts a vehicle sim (`createVehicleDynamics`, `createKinematicVehicle`) into a {@link PhysicsBackend} world without
|
|
21
|
+
* handing its handling to the rigid-body solver: the sim still owns grip and balance, while a kinematic chassis box
|
|
22
|
+
* follows it so Rapier or `PhysicsWorld` props get shoved, and `clampMove` sweeps that box so walls and parked cars
|
|
23
|
+
* stop it and it slides along them.
|
|
24
|
+
* @capability vehicle-backend-link collide a vehicle sim with a physics backend — shove props, stop at walls, slide along them
|
|
25
|
+
*/
|
|
26
|
+
export function createVehicleBackendLink(backend, options) {
|
|
27
|
+
const half = options.halfExtents;
|
|
28
|
+
const clearance = options.clearance ?? 0.1;
|
|
29
|
+
const groundNormalY = options.groundNormalY ?? 0.7;
|
|
30
|
+
const shape = { kind: "box", halfExtents: half };
|
|
31
|
+
const pose = { baseY: 0, heading: 0, pitch: 0, roll: 0 };
|
|
32
|
+
const hit = { active: false, value: { body: -1, normal: [0, 0, 0], blocked: 0 } };
|
|
33
|
+
const body = backend.addBody({
|
|
34
|
+
shape,
|
|
35
|
+
kind: "kinematic",
|
|
36
|
+
position: [0, half[1] + clearance, 0],
|
|
37
|
+
layers: options.layers ?? 1,
|
|
38
|
+
...(options.mask === undefined ? {} : { mask: options.mask }),
|
|
39
|
+
});
|
|
40
|
+
function sweep(x, z, dx, dz) {
|
|
41
|
+
return backend.shapecast({
|
|
42
|
+
shape,
|
|
43
|
+
position: [x, pose.baseY + half[1] + clearance, z],
|
|
44
|
+
rotation: poseQuat(pose.heading, 0, 0),
|
|
45
|
+
motion: [dx, 0, dz],
|
|
46
|
+
exclude: body,
|
|
47
|
+
...(options.blockMask === undefined ? {} : { mask: options.blockMask }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
body,
|
|
52
|
+
clampMove(from, to) {
|
|
53
|
+
hit.active = false;
|
|
54
|
+
let x = from[0];
|
|
55
|
+
let z = from[1];
|
|
56
|
+
let dx = to[0] - x;
|
|
57
|
+
let dz = to[1] - z;
|
|
58
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
59
|
+
const length = Math.hypot(dx, dz);
|
|
60
|
+
if (length < 1e-9)
|
|
61
|
+
break;
|
|
62
|
+
const found = sweep(x, z, dx, dz);
|
|
63
|
+
if (found === null || found.normal[1] > groundNormalY) {
|
|
64
|
+
x += dx;
|
|
65
|
+
z += dz;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const travel = Math.max(0, found.toi - 0.02 / length);
|
|
69
|
+
x += dx * travel;
|
|
70
|
+
z += dz * travel;
|
|
71
|
+
const nx = found.normal[0];
|
|
72
|
+
const nz = found.normal[2];
|
|
73
|
+
const flat = Math.hypot(nx, nz) || 1;
|
|
74
|
+
const restX = dx * (1 - travel);
|
|
75
|
+
const restZ = dz * (1 - travel);
|
|
76
|
+
const into = (restX * nx + restZ * nz) / flat;
|
|
77
|
+
hit.active = true;
|
|
78
|
+
hit.value = { body: found.body, normal: [nx / flat, 0, nz / flat], blocked: Math.abs(into) };
|
|
79
|
+
dx = restX - (into * nx) / flat;
|
|
80
|
+
dz = restZ - (into * nz) / flat;
|
|
81
|
+
}
|
|
82
|
+
return [x, z];
|
|
83
|
+
},
|
|
84
|
+
sync(next) {
|
|
85
|
+
pose.baseY = next.position[1];
|
|
86
|
+
pose.heading = next.heading;
|
|
87
|
+
pose.pitch = next.bodyPitch ?? 0;
|
|
88
|
+
pose.roll = next.bodyRoll ?? 0;
|
|
89
|
+
const center = [next.position[0], next.position[1] + half[1] + clearance, next.position[2]];
|
|
90
|
+
const rotation = poseQuat(pose.heading, pose.pitch, pose.roll);
|
|
91
|
+
backend.setKinematicTarget(body, center, rotation);
|
|
92
|
+
// A resting prop sleeps, and a kinematic body moved by target does not wake it on every backend.
|
|
93
|
+
for (const touched of backend.overlap({ shape, position: center, rotation, exclude: body }))
|
|
94
|
+
backend.wake(touched);
|
|
95
|
+
},
|
|
96
|
+
lastHit: () => (hit.active ? hit.value : null),
|
|
97
|
+
dispose() {
|
|
98
|
+
if (backend.hasBody(body))
|
|
99
|
+
backend.removeBody(body);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|