@jgengine/core 0.18.0 → 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 +265 -2
- package/dist/ai/decisionGraph.d.ts +96 -0
- package/dist/ai/decisionGraph.js +212 -0
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +155 -0
- package/dist/anim/animGraph.js +414 -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/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +35 -0
- package/dist/anim/locomotionGraph.js +62 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +12 -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/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +92 -1
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +24 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +20 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +59 -1
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +14 -1
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +167 -6
- package/dist/game/systemRuntime.js +6 -1
- 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 +17 -2
- package/dist/input/actionBindings.js +24 -11
- package/dist/input/actionContexts.d.ts +45 -0
- package/dist/input/actionContexts.js +113 -0
- 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 +79 -0
- package/dist/input/gamepadModel.js +108 -0
- 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 +196 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- 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 +30 -3
- package/dist/movement/playerMovement.js +304 -7
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +39 -8
- package/dist/movement/solidObstacles.js +71 -6
- package/dist/movement/voxelController.js +7 -7
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +74 -0
- package/dist/nav/navMesh.js +570 -0
- 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 +29 -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 +37 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +203 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +151 -0
- package/dist/physics/physicsWorldBackend.d.ts +45 -0
- package/dist/physics/physicsWorldBackend.js +518 -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 +13 -0
- package/dist/physics/worldColliders.js +128 -0
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +19 -6
- package/dist/render/lookPreset.js +56 -4
- package/dist/render/postProcessing.d.ts +40 -1
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +52 -0
- package/dist/runtime/commandInput.js +71 -0
- package/dist/runtime/commandRunner.d.ts +39 -7
- package/dist/runtime/commandRunner.js +35 -5
- 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 +18 -1
- package/dist/runtime/gameContextTypes.d.ts +33 -7
- package/dist/runtime/gameRuntime.d.ts +23 -5
- package/dist/runtime/gameRuntime.js +21 -4
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +14 -0
- package/dist/runtime/inputSnapshot.js +11 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +41 -2
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +29 -1
- package/dist/scene/behaviorRuntime.js +71 -4
- package/dist/scene/behaviors.d.ts +12 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- 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 +70 -0
- package/dist/sensor/perception.js +189 -0
- 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 +21 -0
- package/dist/settings/graphicsProfile.js +48 -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/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -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 +46 -2
- package/dist/world/buildings.js +53 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +139 -0
- package/dist/world/cityKind.js +367 -16
- package/dist/world/features.d.ts +56 -3
- package/dist/world/features.js +13 -2
- 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/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -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 +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -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/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +501 -453
- package/skills/jgengine-gameplay/capabilities.md +36 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- 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
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { IDENTITY_QUAT, isIdentityQuat, shapeHalfExtents, } from "./physicsBackend.js";
|
|
2
|
+
import { PhysicsWorld } from "./physicsWorld.js";
|
|
3
|
+
const ALL_LAYERS = 0xffffffff;
|
|
4
|
+
const CAPABILITIES = {
|
|
5
|
+
rotation: false,
|
|
6
|
+
shapes: ["box", "sphere", "capsule", "convex", "trimesh", "heightfield"],
|
|
7
|
+
ccd: false,
|
|
8
|
+
joints: ["fixed", "hinge", "ball", "distance", "spring"],
|
|
9
|
+
};
|
|
10
|
+
function slabRay(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ) {
|
|
11
|
+
let tMin = 0;
|
|
12
|
+
let tMax = Number.POSITIVE_INFINITY;
|
|
13
|
+
let axis = -1;
|
|
14
|
+
let sign = 0;
|
|
15
|
+
const origin = [ox, oy, oz];
|
|
16
|
+
const dir = [dx, dy, dz];
|
|
17
|
+
const mins = [minX, minY, minZ];
|
|
18
|
+
const maxs = [maxX, maxY, maxZ];
|
|
19
|
+
for (let i = 0; i < 3; i += 1) {
|
|
20
|
+
const o = origin[i];
|
|
21
|
+
const d = dir[i];
|
|
22
|
+
if (Math.abs(d) < 1e-12) {
|
|
23
|
+
if (o < mins[i] || o > maxs[i])
|
|
24
|
+
return null;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const inv = 1 / d;
|
|
28
|
+
let t1 = (mins[i] - o) * inv;
|
|
29
|
+
let t2 = (maxs[i] - o) * inv;
|
|
30
|
+
let entrySign = -1;
|
|
31
|
+
if (t1 > t2) {
|
|
32
|
+
const tmp = t1;
|
|
33
|
+
t1 = t2;
|
|
34
|
+
t2 = tmp;
|
|
35
|
+
entrySign = 1;
|
|
36
|
+
}
|
|
37
|
+
// `>=` so a shape resting exactly on a face still reports the contact at t=0 (grounding needs it).
|
|
38
|
+
if (t1 >= tMin) {
|
|
39
|
+
tMin = t1;
|
|
40
|
+
axis = i;
|
|
41
|
+
sign = entrySign;
|
|
42
|
+
}
|
|
43
|
+
tMax = Math.min(tMax, t2);
|
|
44
|
+
if (tMin > tMax)
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { t: tMin, axis, sign };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `PhysicsWorld` behind the {@link PhysicsBackend} interface: zero dependencies, translation-only. Capsules,
|
|
51
|
+
* convex hulls, meshes, and heightfields collide as their bounding boxes; rotation, angular velocity, CCD, and
|
|
52
|
+
* hinge limits are accepted and ignored (see {@link PhysicsBackend.capabilities}). Reach for `@jgengine/rapier`
|
|
53
|
+
* when a game needs those for real; the seam is the same.
|
|
54
|
+
*
|
|
55
|
+
* @capability physics-world-backend the zero-dependency PhysicsWorld solver exposed through the PhysicsBackend seam
|
|
56
|
+
*/
|
|
57
|
+
export function createPhysicsWorldBackend(options) {
|
|
58
|
+
const world = new PhysicsWorld({
|
|
59
|
+
capacity: options.capacity,
|
|
60
|
+
bounds: options.bounds,
|
|
61
|
+
...(options.gravity === undefined ? {} : { gravity: options.gravity[1] }),
|
|
62
|
+
...(options.fixedDt === undefined ? {} : { fixedDt: options.fixedDt }),
|
|
63
|
+
...(options.maxSubsteps === undefined ? {} : { maxSubsteps: options.maxSubsteps }),
|
|
64
|
+
...(options.precision === undefined ? {} : { precision: options.precision }),
|
|
65
|
+
...(options.friction === undefined ? {} : { friction: options.friction }),
|
|
66
|
+
...(options.restitution === undefined ? {} : { restitution: options.restitution }),
|
|
67
|
+
});
|
|
68
|
+
const records = new Map();
|
|
69
|
+
const handleOfIndex = new Map();
|
|
70
|
+
const joints = new Map();
|
|
71
|
+
const masses = new Map();
|
|
72
|
+
let nextHandle = 1;
|
|
73
|
+
let nextJoint = 1;
|
|
74
|
+
let warnedRotation = false;
|
|
75
|
+
let warnedShape = false;
|
|
76
|
+
let contactListener = null;
|
|
77
|
+
const contactEvent = {
|
|
78
|
+
a: 0,
|
|
79
|
+
b: 0,
|
|
80
|
+
normal: [0, 0, 0],
|
|
81
|
+
approachSpeed: 0,
|
|
82
|
+
impulse: 0,
|
|
83
|
+
};
|
|
84
|
+
const warn = options.warn ?? true;
|
|
85
|
+
function noteRotation(rotation) {
|
|
86
|
+
if (warnedRotation || !warn || isIdentityQuat(rotation))
|
|
87
|
+
return;
|
|
88
|
+
warnedRotation = true;
|
|
89
|
+
console.warn("[jgengine:physics] PhysicsWorld backend ignores rotation; use @jgengine/rapier for rotating bodies.");
|
|
90
|
+
}
|
|
91
|
+
function noteShape(shape) {
|
|
92
|
+
if (warnedShape || !warn || shape.kind === "box" || shape.kind === "sphere")
|
|
93
|
+
return;
|
|
94
|
+
warnedShape = true;
|
|
95
|
+
console.warn(`[jgengine:physics] PhysicsWorld backend collides "${shape.kind}" shapes as their bounding box.`);
|
|
96
|
+
}
|
|
97
|
+
function record(handle) {
|
|
98
|
+
const rec = records.get(handle);
|
|
99
|
+
if (rec === undefined)
|
|
100
|
+
throw new Error(`physics: unknown body handle ${handle}`);
|
|
101
|
+
return rec;
|
|
102
|
+
}
|
|
103
|
+
function insert(handle, desc, sleeping) {
|
|
104
|
+
const kind = desc.kind ?? "dynamic";
|
|
105
|
+
const half = shapeHalfExtents(desc.shape);
|
|
106
|
+
const index = world.addBody(desc.shape.kind === "sphere"
|
|
107
|
+
? {
|
|
108
|
+
shape: "sphere",
|
|
109
|
+
radius: desc.shape.radius,
|
|
110
|
+
position: desc.position,
|
|
111
|
+
...(desc.velocity === undefined ? {} : { velocity: desc.velocity }),
|
|
112
|
+
mass: desc.mass ?? 1,
|
|
113
|
+
static: kind !== "dynamic",
|
|
114
|
+
asleep: sleeping,
|
|
115
|
+
}
|
|
116
|
+
: {
|
|
117
|
+
shape: "box",
|
|
118
|
+
halfExtents: half,
|
|
119
|
+
position: desc.position,
|
|
120
|
+
...(desc.velocity === undefined ? {} : { velocity: desc.velocity }),
|
|
121
|
+
mass: desc.mass ?? 1,
|
|
122
|
+
static: kind !== "dynamic",
|
|
123
|
+
asleep: sleeping,
|
|
124
|
+
});
|
|
125
|
+
records.set(handle, {
|
|
126
|
+
index,
|
|
127
|
+
shape: desc.shape,
|
|
128
|
+
kind,
|
|
129
|
+
layers: desc.layers ?? 1,
|
|
130
|
+
mask: desc.mask ?? ALL_LAYERS,
|
|
131
|
+
userData: desc.userData,
|
|
132
|
+
half,
|
|
133
|
+
kinematicVelocity: [0, 0, 0],
|
|
134
|
+
kinematicTarget: null,
|
|
135
|
+
linearDamping: desc.linearDamping ?? 0,
|
|
136
|
+
force: [0, 0, 0],
|
|
137
|
+
});
|
|
138
|
+
handleOfIndex.set(index, handle);
|
|
139
|
+
masses.set(handle, desc.mass ?? 1);
|
|
140
|
+
}
|
|
141
|
+
function addJointToWorld(desc) {
|
|
142
|
+
const a = record(desc.bodyA).index;
|
|
143
|
+
const base = {
|
|
144
|
+
bodyA: a,
|
|
145
|
+
...(desc.bodyB === undefined ? {} : { bodyB: record(desc.bodyB).index }),
|
|
146
|
+
...(desc.anchorA === undefined ? {} : { anchorA: desc.anchorA }),
|
|
147
|
+
...(desc.anchorB === undefined ? {} : { anchorB: desc.anchorB }),
|
|
148
|
+
...(desc.restLength === undefined ? {} : { restLength: desc.restLength }),
|
|
149
|
+
...(desc.stiffness === undefined ? {} : { stiffness: desc.stiffness }),
|
|
150
|
+
...(desc.damping === undefined ? {} : { damping: desc.damping }),
|
|
151
|
+
...(desc.axis === undefined ? {} : { axis: desc.axis }),
|
|
152
|
+
};
|
|
153
|
+
switch (desc.kind) {
|
|
154
|
+
case "hinge":
|
|
155
|
+
return world.hingeJoint(base);
|
|
156
|
+
case "distance":
|
|
157
|
+
return world.distanceJoint(base);
|
|
158
|
+
case "spring":
|
|
159
|
+
return world.springJoint(base);
|
|
160
|
+
default:
|
|
161
|
+
return world.fixedJoint(base);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function bodyMatches(rec, mask, exclude, handle) {
|
|
165
|
+
if (exclude === handle)
|
|
166
|
+
return false;
|
|
167
|
+
if (mask !== undefined && (rec.layers & mask) === 0)
|
|
168
|
+
return false;
|
|
169
|
+
return world.isAlive(rec.index);
|
|
170
|
+
}
|
|
171
|
+
world.onCollision((event) => {
|
|
172
|
+
if (contactListener === null)
|
|
173
|
+
return;
|
|
174
|
+
const a = handleOfIndex.get(event.a);
|
|
175
|
+
const b = handleOfIndex.get(event.b);
|
|
176
|
+
if (a === undefined || b === undefined)
|
|
177
|
+
return;
|
|
178
|
+
contactEvent.a = a;
|
|
179
|
+
contactEvent.b = b;
|
|
180
|
+
contactEvent.normal[0] = event.nx;
|
|
181
|
+
contactEvent.normal[1] = event.ny;
|
|
182
|
+
contactEvent.normal[2] = event.nz;
|
|
183
|
+
contactEvent.approachSpeed = event.approachSpeed;
|
|
184
|
+
contactEvent.impulse = event.impulse;
|
|
185
|
+
contactListener(contactEvent);
|
|
186
|
+
});
|
|
187
|
+
const backend = {
|
|
188
|
+
name: "physics-world",
|
|
189
|
+
capabilities: CAPABILITIES,
|
|
190
|
+
addBody(desc) {
|
|
191
|
+
noteRotation(desc.rotation);
|
|
192
|
+
noteShape(desc.shape);
|
|
193
|
+
const handle = nextHandle;
|
|
194
|
+
nextHandle += 1;
|
|
195
|
+
insert(handle, desc, desc.asleep === true);
|
|
196
|
+
return handle;
|
|
197
|
+
},
|
|
198
|
+
removeBody(handle) {
|
|
199
|
+
const rec = records.get(handle);
|
|
200
|
+
if (rec === undefined)
|
|
201
|
+
return;
|
|
202
|
+
for (const [jointHandle, joint] of joints) {
|
|
203
|
+
if (joint.desc.bodyA === handle || joint.desc.bodyB === handle) {
|
|
204
|
+
world.removeJoint(joint.id);
|
|
205
|
+
joints.delete(jointHandle);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
world.removeBody(rec.index);
|
|
209
|
+
handleOfIndex.delete(rec.index);
|
|
210
|
+
records.delete(handle);
|
|
211
|
+
masses.delete(handle);
|
|
212
|
+
},
|
|
213
|
+
hasBody: (handle) => records.has(handle),
|
|
214
|
+
body(handle, out) {
|
|
215
|
+
const rec = records.get(handle);
|
|
216
|
+
if (rec === undefined)
|
|
217
|
+
return null;
|
|
218
|
+
const i = rec.index;
|
|
219
|
+
const velocity = rec.kind === "kinematic" ? rec.kinematicVelocity : [world.velX[i], world.velY[i], world.velZ[i]];
|
|
220
|
+
const state = out ?? {
|
|
221
|
+
position: [0, 0, 0],
|
|
222
|
+
rotation: IDENTITY_QUAT,
|
|
223
|
+
velocity: [0, 0, 0],
|
|
224
|
+
angularVelocity: [0, 0, 0],
|
|
225
|
+
sleeping: false,
|
|
226
|
+
};
|
|
227
|
+
state.position = [world.posX[i], world.posY[i], world.posZ[i]];
|
|
228
|
+
state.rotation = IDENTITY_QUAT;
|
|
229
|
+
state.velocity = velocity;
|
|
230
|
+
state.angularVelocity = [0, 0, 0];
|
|
231
|
+
state.sleeping = world.isSleeping(i);
|
|
232
|
+
return state;
|
|
233
|
+
},
|
|
234
|
+
userDataOf: (handle) => records.get(handle)?.userData,
|
|
235
|
+
setPosition(handle, position) {
|
|
236
|
+
world.setPosition(record(handle).index, position[0], position[1], position[2]);
|
|
237
|
+
},
|
|
238
|
+
setRotation(_handle, rotation) {
|
|
239
|
+
noteRotation(rotation);
|
|
240
|
+
},
|
|
241
|
+
setVelocity(handle, velocity) {
|
|
242
|
+
world.setVelocity(record(handle).index, velocity[0], velocity[1], velocity[2]);
|
|
243
|
+
},
|
|
244
|
+
setAngularVelocity() { },
|
|
245
|
+
applyImpulse(handle, impulse) {
|
|
246
|
+
const rec = record(handle);
|
|
247
|
+
if (rec.kind !== "dynamic")
|
|
248
|
+
return;
|
|
249
|
+
const i = rec.index;
|
|
250
|
+
const invMass = world.invMass[i];
|
|
251
|
+
world.setVelocity(i, world.velX[i] + impulse[0] * invMass, world.velY[i] + impulse[1] * invMass, world.velZ[i] + impulse[2] * invMass);
|
|
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
|
+
},
|
|
264
|
+
setKinematicTarget(handle, position) {
|
|
265
|
+
const rec = record(handle);
|
|
266
|
+
rec.kinematicTarget = [position[0], position[1], position[2]];
|
|
267
|
+
},
|
|
268
|
+
wake(handle) {
|
|
269
|
+
world.wake(record(handle).index);
|
|
270
|
+
},
|
|
271
|
+
addJoint(desc) {
|
|
272
|
+
const handle = nextJoint;
|
|
273
|
+
nextJoint += 1;
|
|
274
|
+
joints.set(handle, { id: addJointToWorld(desc), desc });
|
|
275
|
+
return handle;
|
|
276
|
+
},
|
|
277
|
+
removeJoint(handle) {
|
|
278
|
+
const joint = joints.get(handle);
|
|
279
|
+
if (joint === undefined)
|
|
280
|
+
return;
|
|
281
|
+
world.removeJoint(joint.id);
|
|
282
|
+
joints.delete(handle);
|
|
283
|
+
},
|
|
284
|
+
step(dt) {
|
|
285
|
+
if (!(dt > 0))
|
|
286
|
+
return;
|
|
287
|
+
for (const rec of records.values()) {
|
|
288
|
+
if (rec.kind !== "kinematic")
|
|
289
|
+
continue;
|
|
290
|
+
const target = rec.kinematicTarget;
|
|
291
|
+
const i = rec.index;
|
|
292
|
+
if (target === null) {
|
|
293
|
+
rec.kinematicVelocity[0] = 0;
|
|
294
|
+
rec.kinematicVelocity[1] = 0;
|
|
295
|
+
rec.kinematicVelocity[2] = 0;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
rec.kinematicVelocity[0] = (target[0] - world.posX[i]) / dt;
|
|
299
|
+
rec.kinematicVelocity[1] = (target[1] - world.posY[i]) / dt;
|
|
300
|
+
rec.kinematicVelocity[2] = (target[2] - world.posZ[i]) / dt;
|
|
301
|
+
world.setPosition(i, target[0], target[1], target[2]);
|
|
302
|
+
rec.kinematicTarget = null;
|
|
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
|
+
}
|
|
322
|
+
world.step(dt);
|
|
323
|
+
},
|
|
324
|
+
raycast(desc) {
|
|
325
|
+
const len = Math.hypot(desc.direction[0], desc.direction[1], desc.direction[2]);
|
|
326
|
+
if (!(len > 0))
|
|
327
|
+
return null;
|
|
328
|
+
const dx = desc.direction[0] / len;
|
|
329
|
+
const dy = desc.direction[1] / len;
|
|
330
|
+
const dz = desc.direction[2] / len;
|
|
331
|
+
let best = null;
|
|
332
|
+
for (const [handle, rec] of records) {
|
|
333
|
+
if (!bodyMatches(rec, desc.mask, desc.exclude, handle))
|
|
334
|
+
continue;
|
|
335
|
+
const i = rec.index;
|
|
336
|
+
const px = world.posX[i];
|
|
337
|
+
const py = world.posY[i];
|
|
338
|
+
const pz = world.posZ[i];
|
|
339
|
+
let t;
|
|
340
|
+
let normal;
|
|
341
|
+
if (rec.shape.kind === "sphere") {
|
|
342
|
+
const r = rec.shape.radius;
|
|
343
|
+
const ox = desc.origin[0] - px;
|
|
344
|
+
const oy = desc.origin[1] - py;
|
|
345
|
+
const oz = desc.origin[2] - pz;
|
|
346
|
+
const b = ox * dx + oy * dy + oz * dz;
|
|
347
|
+
const c = ox * ox + oy * oy + oz * oz - r * r;
|
|
348
|
+
const disc = b * b - c;
|
|
349
|
+
if (disc < 0)
|
|
350
|
+
continue;
|
|
351
|
+
t = -b - Math.sqrt(disc);
|
|
352
|
+
if (t < 0)
|
|
353
|
+
continue;
|
|
354
|
+
const hx = ox + dx * t;
|
|
355
|
+
const hy = oy + dy * t;
|
|
356
|
+
const hz = oz + dz * t;
|
|
357
|
+
normal = [hx / r, hy / r, hz / r];
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const hit = slabRay(desc.origin[0], desc.origin[1], desc.origin[2], dx, dy, dz, px - rec.half[0], py - rec.half[1], pz - rec.half[2], px + rec.half[0], py + rec.half[1], pz + rec.half[2]);
|
|
361
|
+
if (hit === null || hit.axis < 0)
|
|
362
|
+
continue;
|
|
363
|
+
t = hit.t;
|
|
364
|
+
normal = [hit.axis === 0 ? hit.sign : 0, hit.axis === 1 ? hit.sign : 0, hit.axis === 2 ? hit.sign : 0];
|
|
365
|
+
}
|
|
366
|
+
if (t > desc.maxDistance)
|
|
367
|
+
continue;
|
|
368
|
+
if (best !== null && t >= best.distance)
|
|
369
|
+
continue;
|
|
370
|
+
best = {
|
|
371
|
+
body: handle,
|
|
372
|
+
distance: t,
|
|
373
|
+
point: [desc.origin[0] + dx * t, desc.origin[1] + dy * t, desc.origin[2] + dz * t],
|
|
374
|
+
normal,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return best;
|
|
378
|
+
},
|
|
379
|
+
shapecast(desc) {
|
|
380
|
+
noteRotation(desc.rotation);
|
|
381
|
+
const castHalf = shapeHalfExtents(desc.shape);
|
|
382
|
+
const [mx, my, mz] = desc.motion;
|
|
383
|
+
const motionLen = Math.hypot(mx, my, mz);
|
|
384
|
+
let best = null;
|
|
385
|
+
for (const [handle, rec] of records) {
|
|
386
|
+
if (!bodyMatches(rec, desc.mask, desc.exclude, handle))
|
|
387
|
+
continue;
|
|
388
|
+
const i = rec.index;
|
|
389
|
+
const minX = world.posX[i] - rec.half[0] - castHalf[0];
|
|
390
|
+
const minY = world.posY[i] - rec.half[1] - castHalf[1];
|
|
391
|
+
const minZ = world.posZ[i] - rec.half[2] - castHalf[2];
|
|
392
|
+
const maxX = world.posX[i] + rec.half[0] + castHalf[0];
|
|
393
|
+
const maxY = world.posY[i] + rec.half[1] + castHalf[1];
|
|
394
|
+
const maxZ = world.posZ[i] + rec.half[2] + castHalf[2];
|
|
395
|
+
const [ox, oy, oz] = desc.position;
|
|
396
|
+
const inside = ox > minX && ox < maxX && oy > minY && oy < maxY && oz > minZ && oz < maxZ;
|
|
397
|
+
let toi;
|
|
398
|
+
let normal;
|
|
399
|
+
if (inside) {
|
|
400
|
+
const pens = [ox - minX, maxX - ox, oy - minY, maxY - oy, oz - minZ, maxZ - oz];
|
|
401
|
+
let least = 0;
|
|
402
|
+
for (let k = 1; k < 6; k += 1)
|
|
403
|
+
if (pens[k] < pens[least])
|
|
404
|
+
least = k;
|
|
405
|
+
toi = 0;
|
|
406
|
+
normal = [
|
|
407
|
+
least === 0 ? -1 : least === 1 ? 1 : 0,
|
|
408
|
+
least === 2 ? -1 : least === 3 ? 1 : 0,
|
|
409
|
+
least === 4 ? -1 : least === 5 ? 1 : 0,
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
if (!(motionLen > 0))
|
|
414
|
+
continue;
|
|
415
|
+
const hit = slabRay(ox, oy, oz, mx, my, mz, minX, minY, minZ, maxX, maxY, maxZ);
|
|
416
|
+
if (hit === null || hit.axis < 0 || hit.t > 1)
|
|
417
|
+
continue;
|
|
418
|
+
toi = hit.t;
|
|
419
|
+
normal = [hit.axis === 0 ? hit.sign : 0, hit.axis === 1 ? hit.sign : 0, hit.axis === 2 ? hit.sign : 0];
|
|
420
|
+
}
|
|
421
|
+
if (best !== null && toi >= best.toi)
|
|
422
|
+
continue;
|
|
423
|
+
best = {
|
|
424
|
+
body: handle,
|
|
425
|
+
toi,
|
|
426
|
+
distance: toi * motionLen,
|
|
427
|
+
point: [ox + mx * toi - normal[0] * castHalf[0], oy + my * toi - normal[1] * castHalf[1], oz + mz * toi - normal[2] * castHalf[2]],
|
|
428
|
+
normal,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return best;
|
|
432
|
+
},
|
|
433
|
+
overlap(desc) {
|
|
434
|
+
const half = shapeHalfExtents(desc.shape);
|
|
435
|
+
const out = [];
|
|
436
|
+
for (const [handle, rec] of records) {
|
|
437
|
+
if (!bodyMatches(rec, desc.mask, desc.exclude, handle))
|
|
438
|
+
continue;
|
|
439
|
+
const i = rec.index;
|
|
440
|
+
if (Math.abs(world.posX[i] - desc.position[0]) >= rec.half[0] + half[0])
|
|
441
|
+
continue;
|
|
442
|
+
if (Math.abs(world.posY[i] - desc.position[1]) >= rec.half[1] + half[1])
|
|
443
|
+
continue;
|
|
444
|
+
if (Math.abs(world.posZ[i] - desc.position[2]) >= rec.half[2] + half[2])
|
|
445
|
+
continue;
|
|
446
|
+
out.push(handle);
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
},
|
|
450
|
+
onContact(listener) {
|
|
451
|
+
contactListener = listener;
|
|
452
|
+
},
|
|
453
|
+
retune() {
|
|
454
|
+
// PhysicsWorld fixes its solver knobs at construction; retune is a no-op here and a real call on native backends.
|
|
455
|
+
},
|
|
456
|
+
snapshot() {
|
|
457
|
+
const bodies = [];
|
|
458
|
+
for (const [handle, rec] of records) {
|
|
459
|
+
const i = rec.index;
|
|
460
|
+
bodies.push({
|
|
461
|
+
handle,
|
|
462
|
+
shape: rec.shape,
|
|
463
|
+
kind: rec.kind,
|
|
464
|
+
layers: rec.layers,
|
|
465
|
+
mask: rec.mask,
|
|
466
|
+
userData: rec.userData,
|
|
467
|
+
mass: masses.get(handle) ?? 1,
|
|
468
|
+
position: [world.posX[i], world.posY[i], world.posZ[i]],
|
|
469
|
+
velocity: [world.velX[i], world.velY[i], world.velZ[i]],
|
|
470
|
+
sleeping: world.isSleeping(i),
|
|
471
|
+
...(rec.linearDamping > 0 ? { linearDamping: rec.linearDamping } : {}),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
nextHandle,
|
|
476
|
+
nextJoint,
|
|
477
|
+
bodies,
|
|
478
|
+
joints: Array.from(joints, ([handle, joint]) => ({ handle, desc: joint.desc })),
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
restore(state) {
|
|
482
|
+
const saved = state;
|
|
483
|
+
world.clear();
|
|
484
|
+
records.clear();
|
|
485
|
+
handleOfIndex.clear();
|
|
486
|
+
joints.clear();
|
|
487
|
+
masses.clear();
|
|
488
|
+
nextHandle = saved.nextHandle;
|
|
489
|
+
nextJoint = saved.nextJoint;
|
|
490
|
+
for (const body of saved.bodies) {
|
|
491
|
+
insert(body.handle, {
|
|
492
|
+
shape: body.shape,
|
|
493
|
+
kind: body.kind,
|
|
494
|
+
position: body.position,
|
|
495
|
+
velocity: body.velocity,
|
|
496
|
+
mass: body.mass,
|
|
497
|
+
layers: body.layers,
|
|
498
|
+
mask: body.mask,
|
|
499
|
+
userData: body.userData,
|
|
500
|
+
...(body.linearDamping === undefined ? {} : { linearDamping: body.linearDamping }),
|
|
501
|
+
}, body.sleeping);
|
|
502
|
+
}
|
|
503
|
+
for (const joint of saved.joints) {
|
|
504
|
+
joints.set(joint.handle, { id: addJointToWorld(joint.desc), desc: joint.desc });
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
dispose() {
|
|
508
|
+
world.onCollision(null);
|
|
509
|
+
contactListener = null;
|
|
510
|
+
world.clear();
|
|
511
|
+
records.clear();
|
|
512
|
+
handleOfIndex.clear();
|
|
513
|
+
joints.clear();
|
|
514
|
+
masses.clear();
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
return backend;
|
|
518
|
+
}
|
|
@@ -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
|
+
}
|