@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.
Files changed (220) hide show
  1. package/CHANGELOG.md +158 -7
  2. package/dist/ai/decisionGraph.d.ts +48 -4
  3. package/dist/ai/decisionGraph.js +153 -23
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/pursuit.d.ts +1 -1
  7. package/dist/ai/pursuit.js +1 -1
  8. package/dist/ai/threat.d.ts +8 -0
  9. package/dist/ai/threat.js +10 -0
  10. package/dist/anim/animGraph.d.ts +11 -0
  11. package/dist/anim/animGraph.js +128 -0
  12. package/dist/anim/boneTexture.d.ts +55 -0
  13. package/dist/anim/boneTexture.js +43 -0
  14. package/dist/anim/footPlacement.d.ts +57 -0
  15. package/dist/anim/footPlacement.js +67 -0
  16. package/dist/anim/locomotionGraph.d.ts +9 -0
  17. package/dist/anim/locomotionGraph.js +29 -0
  18. package/dist/audio/audioFalloff.d.ts +2 -0
  19. package/dist/audio/doppler.d.ts +21 -0
  20. package/dist/audio/doppler.js +32 -0
  21. package/dist/audio/engineLayers.d.ts +83 -0
  22. package/dist/audio/engineLayers.js +115 -0
  23. package/dist/combat/abilityKit.d.ts +1 -0
  24. package/dist/combat/abilityKit.js +1 -0
  25. package/dist/combat/death.d.ts +7 -0
  26. package/dist/combat/shotOrigin.d.ts +6 -0
  27. package/dist/combat/shotOrigin.js +6 -0
  28. package/dist/combat/weaponFeedback.d.ts +36 -0
  29. package/dist/combat/weaponFeedback.js +42 -0
  30. package/dist/combat/weaponHandling.d.ts +161 -0
  31. package/dist/combat/weaponHandling.js +202 -0
  32. package/dist/combat/weaponPresentation.d.ts +115 -0
  33. package/dist/combat/weaponPresentation.js +96 -0
  34. package/dist/devtools/warnOnce.d.ts +8 -0
  35. package/dist/devtools/warnOnce.js +18 -0
  36. package/dist/editor/document.js +3 -1
  37. package/dist/editor/environment.js +8 -0
  38. package/dist/editor/types.d.ts +4 -0
  39. package/dist/faction/reputation.d.ts +6 -0
  40. package/dist/faction/reputation.js +11 -0
  41. package/dist/game/cameraConfig.d.ts +51 -1
  42. package/dist/game/events.d.ts +6 -0
  43. package/dist/game/lootTable.d.ts +4 -0
  44. package/dist/game/lootTable.js +16 -0
  45. package/dist/game/playableGame.d.ts +84 -17
  46. package/dist/game/toasts.d.ts +8 -0
  47. package/dist/game/toasts.js +7 -0
  48. package/dist/game/vfxInstance.d.ts +9 -0
  49. package/dist/game/vfxInstance.js +12 -1
  50. package/dist/game/viewports.d.ts +43 -0
  51. package/dist/game/viewports.js +46 -0
  52. package/dist/game/worldItem.d.ts +5 -1
  53. package/dist/game/worldItem.js +0 -1
  54. package/dist/input/actionBindings.d.ts +15 -1
  55. package/dist/input/actionBindings.js +14 -3
  56. package/dist/input/actionContexts.d.ts +25 -1
  57. package/dist/input/actionContexts.js +82 -25
  58. package/dist/input/axisInput.d.ts +4 -0
  59. package/dist/input/axisInput.js +4 -0
  60. package/dist/input/axisShaper.d.ts +73 -0
  61. package/dist/input/axisShaper.js +105 -0
  62. package/dist/input/gamepadModel.d.ts +36 -2
  63. package/dist/input/gamepadModel.js +69 -30
  64. package/dist/input/haptics.d.ts +50 -0
  65. package/dist/input/haptics.js +74 -0
  66. package/dist/input/inputBuffer.d.ts +40 -0
  67. package/dist/input/inputBuffer.js +59 -0
  68. package/dist/input/lookChannel.d.ts +10 -0
  69. package/dist/input/lookChannel.js +6 -0
  70. package/dist/input/pointer.d.ts +2 -0
  71. package/dist/input/pointer.js +4 -0
  72. package/dist/item/affix.d.ts +6 -0
  73. package/dist/item/affix.js +6 -0
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +112 -1
  76. package/dist/movement/glideModel.d.ts +3 -0
  77. package/dist/movement/glideModel.js +6 -0
  78. package/dist/movement/grappleSwing.d.ts +2 -0
  79. package/dist/movement/grappleSwing.js +5 -1
  80. package/dist/movement/movementModel.d.ts +39 -0
  81. package/dist/movement/movementModel.js +62 -19
  82. package/dist/movement/movementProbe.d.ts +51 -0
  83. package/dist/movement/movementProbe.js +119 -0
  84. package/dist/movement/playerMovement.d.ts +21 -2
  85. package/dist/movement/playerMovement.js +89 -6
  86. package/dist/movement/solidObstacles.d.ts +9 -1
  87. package/dist/movement/solidObstacles.js +44 -2
  88. package/dist/movement/voxelController.js +7 -7
  89. package/dist/nav/navFromEnvironment.d.ts +9 -3
  90. package/dist/nav/navFromEnvironment.js +18 -12
  91. package/dist/nav/navMesh.d.ts +43 -2
  92. package/dist/nav/navMesh.js +534 -128
  93. package/dist/physics/aircraftDynamics.d.ts +390 -0
  94. package/dist/physics/aircraftDynamics.js +704 -0
  95. package/dist/physics/boatDynamics.d.ts +116 -0
  96. package/dist/physics/boatDynamics.js +160 -0
  97. package/dist/physics/drivableVehicle.d.ts +22 -8
  98. package/dist/physics/drivableVehicle.js +4 -2
  99. package/dist/physics/flightDynamics.d.ts +21 -4
  100. package/dist/physics/flightDynamics.js +28 -5
  101. package/dist/physics/handlingProbe.d.ts +249 -0
  102. package/dist/physics/handlingProbe.js +606 -0
  103. package/dist/physics/kinematicVehicle.d.ts +32 -2
  104. package/dist/physics/kinematicVehicle.js +15 -0
  105. package/dist/physics/physicsBackend.d.ts +12 -0
  106. package/dist/physics/physicsBackendConformance.js +34 -0
  107. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  108. package/dist/physics/physicsWorldBackend.js +33 -0
  109. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  110. package/dist/physics/vehicleBackendLink.js +102 -0
  111. package/dist/physics/vehicleDynamics.d.ts +374 -0
  112. package/dist/physics/vehicleDynamics.js +737 -0
  113. package/dist/physics/worldColliders.d.ts +1 -1
  114. package/dist/physics/worldColliders.js +35 -3
  115. package/dist/puzzle/cellGrid.d.ts +15 -9
  116. package/dist/puzzle/cellGrid.js +13 -9
  117. package/dist/render/lookPreset.d.ts +9 -2
  118. package/dist/render/lookPreset.js +29 -2
  119. package/dist/render/postProcessing.d.ts +30 -0
  120. package/dist/render/postProcessing.js +1 -1
  121. package/dist/runtime/cameraDirector.d.ts +17 -3
  122. package/dist/runtime/cameraDirector.js +25 -0
  123. package/dist/runtime/commandInput.d.ts +37 -0
  124. package/dist/runtime/commandInput.js +58 -0
  125. package/dist/runtime/commandRunner.d.ts +27 -0
  126. package/dist/runtime/commandRunner.js +31 -3
  127. package/dist/runtime/context/combat.d.ts +1 -0
  128. package/dist/runtime/context/combat.js +2 -1
  129. package/dist/runtime/context/deathLoot.d.ts +2 -0
  130. package/dist/runtime/context/deathLoot.js +1 -0
  131. package/dist/runtime/gameContext.js +7 -0
  132. package/dist/runtime/gameContextTypes.d.ts +19 -3
  133. package/dist/runtime/gameRuntime.d.ts +3 -1
  134. package/dist/runtime/gameRuntime.js +4 -0
  135. package/dist/runtime/inputSnapshot.d.ts +6 -0
  136. package/dist/runtime/inputSnapshot.js +10 -0
  137. package/dist/runtime/localPlayers.d.ts +73 -0
  138. package/dist/runtime/localPlayers.js +119 -0
  139. package/dist/runtime/transport.d.ts +9 -0
  140. package/dist/runtime/transport.js +7 -1
  141. package/dist/scene/behaviorRuntime.d.ts +12 -2
  142. package/dist/scene/behaviorRuntime.js +30 -14
  143. package/dist/scene/behaviors.d.ts +5 -1
  144. package/dist/scene/sceneKinds.d.ts +9 -0
  145. package/dist/scene/vehicleSeat.d.ts +5 -1
  146. package/dist/scene/vehicleSeat.js +5 -1
  147. package/dist/sensor/concealment.d.ts +6 -0
  148. package/dist/sensor/concealment.js +8 -0
  149. package/dist/sensor/freezeMonitor.d.ts +6 -0
  150. package/dist/sensor/freezeMonitor.js +8 -0
  151. package/dist/sensor/perception.d.ts +11 -1
  152. package/dist/sensor/perception.js +57 -10
  153. package/dist/sensor/recordingBuffer.d.ts +3 -0
  154. package/dist/sensor/recordingBuffer.js +8 -0
  155. package/dist/session/roundState.d.ts +4 -0
  156. package/dist/session/roundState.js +11 -0
  157. package/dist/settings/frameRateLimit.d.ts +16 -0
  158. package/dist/settings/frameRateLimit.js +37 -0
  159. package/dist/settings/graphicsProfile.d.ts +2 -0
  160. package/dist/settings/graphicsProfile.js +12 -0
  161. package/dist/settings/graphicsSettings.d.ts +40 -0
  162. package/dist/settings/graphicsSettings.js +81 -0
  163. package/dist/settings/settingsModel.d.ts +9 -1
  164. package/dist/settings/settingsModel.js +10 -0
  165. package/dist/stats/eventMeter.d.ts +1 -1
  166. package/dist/stats/eventMeter.js +1 -1
  167. package/dist/vfx/feedbackMixer.d.ts +61 -0
  168. package/dist/vfx/feedbackMixer.js +106 -0
  169. package/dist/visibility/simulationCulling.d.ts +8 -1
  170. package/dist/visibility/simulationCulling.js +7 -0
  171. package/dist/visibility/spatialIndex.d.ts +21 -0
  172. package/dist/visibility/spatialIndex.js +16 -0
  173. package/dist/world/authoredSolids.d.ts +46 -0
  174. package/dist/world/authoredSolids.js +75 -0
  175. package/dist/world/buildingGenerator.js +2 -2
  176. package/dist/world/buildingKit.d.ts +3 -1
  177. package/dist/world/buildings.d.ts +34 -2
  178. package/dist/world/buildings.js +11 -1
  179. package/dist/world/cityKind.d.ts +7 -0
  180. package/dist/world/cityKind.js +52 -1
  181. package/dist/world/features.d.ts +29 -2
  182. package/dist/world/features.js +6 -0
  183. package/dist/world/footprintGrid.d.ts +7 -0
  184. package/dist/world/footprintGrid.js +16 -0
  185. package/dist/world/interiors.d.ts +1 -0
  186. package/dist/world/interiors.js +5 -3
  187. package/dist/world/lod.d.ts +6 -0
  188. package/dist/world/lod.js +8 -0
  189. package/dist/world/terraform.d.ts +8 -0
  190. package/dist/world/terraform.js +7 -0
  191. package/dist/world/walls.d.ts +7 -0
  192. package/dist/world/walls.js +7 -0
  193. package/dist/world/worldSolids.d.ts +84 -0
  194. package/dist/world/worldSolids.js +240 -0
  195. package/dist/world.d.ts +2 -2
  196. package/dist/world.js +1 -1
  197. package/package.json +1 -1
  198. package/skills/ce-handoff/SKILL.md +1 -1
  199. package/skills/game-design/SKILL.md +2 -2
  200. package/skills/jgengine/SKILL.md +7 -5
  201. package/skills/jgengine/api.md +118 -89
  202. package/skills/jgengine/capabilities.md +28 -0
  203. package/skills/jgengine/recipes/minimal-game.md +3 -0
  204. package/skills/jgengine-combat/SKILL.md +5 -0
  205. package/skills/jgengine-combat/api.md +138 -103
  206. package/skills/jgengine-combat/capabilities.md +29 -1
  207. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  208. package/skills/jgengine-gameplay/api.md +462 -436
  209. package/skills/jgengine-gameplay/capabilities.md +24 -0
  210. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  211. package/skills/jgengine-world/SKILL.md +15 -3
  212. package/skills/jgengine-world/api.md +873 -626
  213. package/skills/jgengine-world/capabilities.md +130 -6
  214. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  215. package/skills/jgengine-world/recipes/controls.md +44 -0
  216. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  217. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  218. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  219. package/skills/jgengine-world/reference.md +31 -2
  220. 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
+ }