@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.
Files changed (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. 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];
@@ -0,0 +1,203 @@
1
+ /** `[x, y, z]`. */
2
+ export type PhysicsVec3 = readonly [number, number, number];
3
+ /** Unit quaternion `[x, y, z, w]`; `[0, 0, 0, 1]` is identity. */
4
+ export type PhysicsQuat = readonly [number, number, number, number];
5
+ /** The identity rotation. */
6
+ export declare const IDENTITY_QUAT: PhysicsQuat;
7
+ /** Collision shape of a body or a cast. Capsules stand along +Y; a trimesh or heightfield is static-only on every backend. */
8
+ export type BodyShape = {
9
+ kind: "box";
10
+ halfExtents: PhysicsVec3;
11
+ } | {
12
+ kind: "sphere";
13
+ radius: number;
14
+ } | {
15
+ kind: "capsule";
16
+ radius: number;
17
+ halfHeight: number;
18
+ } | {
19
+ kind: "convex";
20
+ points: readonly PhysicsVec3[];
21
+ } | {
22
+ kind: "trimesh";
23
+ vertices: ArrayLike<number>;
24
+ indices: ArrayLike<number>;
25
+ } | {
26
+ kind: "heightfield";
27
+ rows: number;
28
+ columns: number;
29
+ heights: ArrayLike<number>;
30
+ scale: PhysicsVec3;
31
+ };
32
+ /** Shape discriminator, as listed in {@link PhysicsCapabilities.shapes}. */
33
+ export type BodyShapeKind = BodyShape["kind"];
34
+ /** `dynamic` integrates under forces, `kinematic` moves only through {@link PhysicsBackend.setKinematicTarget}, `static` never moves. */
35
+ export type BodyKind = "dynamic" | "kinematic" | "static";
36
+ /** Everything a backend needs to create a body. Rotation-free backends ignore `rotation` and `angularVelocity` and say so in {@link PhysicsCapabilities}. */
37
+ export interface BodyDesc {
38
+ shape: BodyShape;
39
+ /** Default `dynamic`. */
40
+ kind?: BodyKind;
41
+ position: PhysicsVec3;
42
+ rotation?: PhysicsQuat;
43
+ velocity?: PhysicsVec3;
44
+ angularVelocity?: PhysicsVec3;
45
+ /** Default 1; ignored for kinematic and static bodies. */
46
+ mass?: number;
47
+ friction?: number;
48
+ restitution?: number;
49
+ /** Collision layer bitmask this body belongs to. Default `1`. */
50
+ layers?: number;
51
+ /** Collision layer bitmask this body collides with. Default all bits. */
52
+ mask?: number;
53
+ /** Continuous collision detection for fast small bodies (bullets, thrown props). */
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;
59
+ asleep?: boolean;
60
+ userData?: unknown;
61
+ }
62
+ /** Backend-issued body id; stable for the body's lifetime and across snapshot/restore. */
63
+ export type BodyHandle = number;
64
+ /** Backend-issued joint id. */
65
+ export type JointHandle = number;
66
+ /** A body's live pose and motion. Rotation-free backends report identity rotation and zero angular velocity. */
67
+ export interface BodyState {
68
+ position: PhysicsVec3;
69
+ rotation: PhysicsQuat;
70
+ velocity: PhysicsVec3;
71
+ angularVelocity: PhysicsVec3;
72
+ sleeping: boolean;
73
+ }
74
+ /** Joint between two bodies, or between `bodyA` and a world anchor when `bodyB` is omitted. */
75
+ export interface JointDesc {
76
+ kind: "fixed" | "hinge" | "ball" | "distance" | "spring";
77
+ bodyA: BodyHandle;
78
+ bodyB?: BodyHandle;
79
+ /** Anchor in A's local frame. Default origin. */
80
+ anchorA?: PhysicsVec3;
81
+ /** Anchor in B's local frame, or the world point when B is omitted. Default origin. */
82
+ anchorB?: PhysicsVec3;
83
+ /** Hinge axis in world space. */
84
+ axis?: PhysicsVec3;
85
+ /** Hinge angle limits in radians. */
86
+ limits?: readonly [min: number, max: number];
87
+ restLength?: number;
88
+ stiffness?: number;
89
+ damping?: number;
90
+ }
91
+ /** A ray query. */
92
+ export interface RayDesc {
93
+ origin: PhysicsVec3;
94
+ /** Need not be normalized. */
95
+ direction: PhysicsVec3;
96
+ maxDistance: number;
97
+ /** Only bodies whose `layers` intersect this mask are hit. Default all. */
98
+ mask?: number;
99
+ exclude?: BodyHandle;
100
+ }
101
+ /** Nearest surface a ray reached. */
102
+ export interface RayHit {
103
+ body: BodyHandle;
104
+ point: PhysicsVec3;
105
+ normal: PhysicsVec3;
106
+ distance: number;
107
+ }
108
+ /** Sweep `shape` from `position` along `motion` and report the first body it touches. */
109
+ export interface ShapeCastDesc {
110
+ shape: BodyShape;
111
+ position: PhysicsVec3;
112
+ rotation?: PhysicsQuat;
113
+ motion: PhysicsVec3;
114
+ mask?: number;
115
+ exclude?: BodyHandle;
116
+ }
117
+ /** First contact of a shape sweep. */
118
+ export interface ShapeCastHit {
119
+ body: BodyHandle;
120
+ /** Fraction of `motion` travelled at first contact, 0..1. */
121
+ toi: number;
122
+ point: PhysicsVec3;
123
+ normal: PhysicsVec3;
124
+ distance: number;
125
+ }
126
+ /** Which bodies a shape placed at `position` intersects. */
127
+ export interface OverlapDesc {
128
+ shape: BodyShape;
129
+ position: PhysicsVec3;
130
+ rotation?: PhysicsQuat;
131
+ mask?: number;
132
+ exclude?: BodyHandle;
133
+ }
134
+ /** A contact reported during {@link PhysicsBackend.step}; the object is reused between calls, so copy what you keep. */
135
+ export interface ContactEvent {
136
+ a: BodyHandle;
137
+ b: BodyHandle;
138
+ normal: PhysicsVec3;
139
+ approachSpeed: number;
140
+ impulse: number;
141
+ }
142
+ /** What a backend can actually simulate, so a game or the conformance suite can adapt instead of guessing. */
143
+ export interface PhysicsCapabilities {
144
+ rotation: boolean;
145
+ shapes: readonly BodyShapeKind[];
146
+ ccd: boolean;
147
+ joints: readonly JointDesc["kind"][];
148
+ }
149
+ /** Solver-wide knobs a backend accepts at creation and through `retune`. */
150
+ export interface PhysicsBackendConfig {
151
+ gravity?: PhysicsVec3;
152
+ /** Fixed substep length the backend integrates with; the caller's `step(dt)` is accumulated into it. */
153
+ fixedDt?: number;
154
+ maxSubsteps?: number;
155
+ }
156
+ /**
157
+ * The seam every physics consumer talks to: the in-tree `PhysicsWorld` behind `createPhysicsWorldBackend`, or a
158
+ * native solver such as Rapier behind its own adapter. Handles are backend-issued and survive snapshot/restore,
159
+ * so gameplay state can store them.
160
+ *
161
+ * @capability physics-backend one interface over interchangeable rigid-body solvers — bodies, joints, raycast, shapecast, snapshot
162
+ */
163
+ export interface PhysicsBackend {
164
+ readonly name: string;
165
+ readonly capabilities: PhysicsCapabilities;
166
+ addBody(desc: BodyDesc): BodyHandle;
167
+ removeBody(handle: BodyHandle): void;
168
+ hasBody(handle: BodyHandle): boolean;
169
+ /** Read a body's state into `out` (or a fresh object); `null` for an unknown handle. */
170
+ body(handle: BodyHandle, out?: BodyState): BodyState | null;
171
+ userDataOf(handle: BodyHandle): unknown;
172
+ setPosition(handle: BodyHandle, position: PhysicsVec3): void;
173
+ setRotation(handle: BodyHandle, rotation: PhysicsQuat): void;
174
+ setVelocity(handle: BodyHandle, velocity: PhysicsVec3): void;
175
+ setAngularVelocity(handle: BodyHandle, angularVelocity: PhysicsVec3): void;
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;
185
+ /** Move a kinematic body so the solver sees its velocity over the next step (platforms, doors, hands). */
186
+ setKinematicTarget(handle: BodyHandle, position: PhysicsVec3, rotation?: PhysicsQuat): void;
187
+ wake(handle: BodyHandle): void;
188
+ addJoint(desc: JointDesc): JointHandle;
189
+ removeJoint(handle: JointHandle): void;
190
+ step(dt: number): void;
191
+ raycast(desc: RayDesc): RayHit | null;
192
+ shapecast(desc: ShapeCastDesc): ShapeCastHit | null;
193
+ overlap(desc: OverlapDesc): BodyHandle[];
194
+ onContact(listener: ((event: ContactEvent) => void) | null): void;
195
+ retune(config: PhysicsBackendConfig): void;
196
+ snapshot(): unknown;
197
+ restore(state: unknown): void;
198
+ dispose(): void;
199
+ }
200
+ /** Axis-aligned bounds of a shape at identity rotation, as half extents. */
201
+ export declare function shapeHalfExtents(shape: BodyShape): [number, number, number];
202
+ /** True for an absent or identity rotation. */
203
+ export declare function isIdentityQuat(q: PhysicsQuat | undefined): boolean;
@@ -0,0 +1,49 @@
1
+ /** The identity rotation. */
2
+ export const IDENTITY_QUAT = [0, 0, 0, 1];
3
+ /** Axis-aligned bounds of a shape at identity rotation, as half extents. */
4
+ export function shapeHalfExtents(shape) {
5
+ switch (shape.kind) {
6
+ case "box":
7
+ return [shape.halfExtents[0], shape.halfExtents[1], shape.halfExtents[2]];
8
+ case "sphere":
9
+ return [shape.radius, shape.radius, shape.radius];
10
+ case "capsule":
11
+ return [shape.radius, shape.halfHeight + shape.radius, shape.radius];
12
+ case "convex": {
13
+ let x = 0;
14
+ let y = 0;
15
+ let z = 0;
16
+ for (const p of shape.points) {
17
+ x = Math.max(x, Math.abs(p[0]));
18
+ y = Math.max(y, Math.abs(p[1]));
19
+ z = Math.max(z, Math.abs(p[2]));
20
+ }
21
+ return [x, y, z];
22
+ }
23
+ case "trimesh": {
24
+ let x = 0;
25
+ let y = 0;
26
+ let z = 0;
27
+ for (let i = 0; i + 2 < shape.vertices.length; i += 3) {
28
+ x = Math.max(x, Math.abs(shape.vertices[i]));
29
+ y = Math.max(y, Math.abs(shape.vertices[i + 1]));
30
+ z = Math.max(z, Math.abs(shape.vertices[i + 2]));
31
+ }
32
+ return [x, y, z];
33
+ }
34
+ case "heightfield": {
35
+ let maxH = 0;
36
+ for (let i = 0; i < shape.heights.length; i += 1)
37
+ maxH = Math.max(maxH, Math.abs(shape.heights[i]));
38
+ return [
39
+ (shape.scale[0] * (shape.columns - 1)) / 2,
40
+ Math.max(maxH * shape.scale[1], 1e-3),
41
+ (shape.scale[2] * (shape.rows - 1)) / 2,
42
+ ];
43
+ }
44
+ }
45
+ }
46
+ /** True for an absent or identity rotation. */
47
+ export function isIdentityQuat(q) {
48
+ return q === undefined || (q[0] === 0 && q[1] === 0 && q[2] === 0 && Math.abs(q[3] - 1) < 1e-9);
49
+ }
@@ -0,0 +1,27 @@
1
+ import type { PhysicsBackend } from "./physicsBackend.js";
2
+ /** The slice of a test runner the conformance suite needs; `bun:test` satisfies it directly. */
3
+ export interface ConformanceHarness {
4
+ test(name: string, run: () => void): void;
5
+ expect(value: unknown): {
6
+ toBe(expected: unknown): void;
7
+ toBeNull(): void;
8
+ not: {
9
+ toBeNull(): void;
10
+ toBe(expected: unknown): void;
11
+ };
12
+ toBeGreaterThan(expected: number): void;
13
+ toBeLessThan(expected: number): void;
14
+ toBeCloseTo(expected: number, digits?: number): void;
15
+ toEqual(expected: unknown): void;
16
+ toContain(expected: unknown): void;
17
+ };
18
+ }
19
+ /**
20
+ * Behavioral contract every {@link PhysicsBackend} must pass: bodies rest on floors, casts and rays report the
21
+ * first surface with an outward normal, kinematic targets produce velocity, masks filter, and snapshot/restore
22
+ * round-trips with stable handles. Backends declare what they cannot do through `capabilities`; the suite reads it.
23
+ * Expects `create()` to return a fresh backend whose world spans at least ±50 on every axis with a floor at y=0.
24
+ *
25
+ * @capability physics-backend-conformance the shared test contract a physics adapter runs to prove it matches the seam
26
+ */
27
+ export declare function runPhysicsBackendConformance(create: () => PhysicsBackend, harness: ConformanceHarness): void;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Behavioral contract every {@link PhysicsBackend} must pass: bodies rest on floors, casts and rays report the
3
+ * first surface with an outward normal, kinematic targets produce velocity, masks filter, and snapshot/restore
4
+ * round-trips with stable handles. Backends declare what they cannot do through `capabilities`; the suite reads it.
5
+ * Expects `create()` to return a fresh backend whose world spans at least ±50 on every axis with a floor at y=0.
6
+ *
7
+ * @capability physics-backend-conformance the shared test contract a physics adapter runs to prove it matches the seam
8
+ */
9
+ export function runPhysicsBackendConformance(create, harness) {
10
+ const { test, expect } = harness;
11
+ function settle(backend, seconds) {
12
+ const steps = Math.ceil(seconds * 60);
13
+ for (let i = 0; i < steps; i += 1)
14
+ backend.step(1 / 60);
15
+ }
16
+ function floor(backend) {
17
+ return backend.addBody({ shape: { kind: "box", halfExtents: [50, 0.5, 50] }, position: [0, -0.5, 0], kind: "static" });
18
+ }
19
+ test("handles are unique, readable, and removable", () => {
20
+ const backend = create();
21
+ const a = backend.addBody({ shape: { kind: "sphere", radius: 0.5 }, position: [1, 2, 3], userData: "a" });
22
+ const b = backend.addBody({ shape: { kind: "box", halfExtents: [1, 1, 1] }, position: [4, 5, 6] });
23
+ expect(a).not.toBe(b);
24
+ expect(backend.body(a)?.position).toEqual([1, 2, 3]);
25
+ expect(backend.userDataOf(a)).toBe("a");
26
+ backend.removeBody(a);
27
+ expect(backend.hasBody(a)).toBe(false);
28
+ expect(backend.body(a)).toBeNull();
29
+ expect(backend.hasBody(b)).toBe(true);
30
+ });
31
+ test("a dynamic box falls onto a static floor and rests", () => {
32
+ const backend = create();
33
+ floor(backend);
34
+ const crate = backend.addBody({ shape: { kind: "box", halfExtents: [0.5, 0.5, 0.5] }, position: [0, 4, 0] });
35
+ settle(backend, 3);
36
+ const state = backend.body(crate);
37
+ expect(state.position[1]).toBeGreaterThan(0.4);
38
+ expect(state.position[1]).toBeLessThan(0.7);
39
+ expect(Math.abs(state.velocity[1])).toBeLessThan(0.5);
40
+ });
41
+ test("raycast reports the floor with an upward normal and the right distance", () => {
42
+ const backend = create();
43
+ const ground = floor(backend);
44
+ const hit = backend.raycast({ origin: [0, 3, 0], direction: [0, -1, 0], maxDistance: 10 });
45
+ expect(hit).not.toBeNull();
46
+ expect(hit.body).toBe(ground);
47
+ expect(hit.distance).toBeCloseTo(3, 3);
48
+ expect(hit.normal[1]).toBeCloseTo(1, 3);
49
+ expect(backend.raycast({ origin: [0, 3, 0], direction: [0, -1, 0], maxDistance: 1 })).toBeNull();
50
+ });
51
+ test("shapecast stops at a wall with a normal facing the mover", () => {
52
+ const backend = create();
53
+ backend.addBody({ shape: { kind: "box", halfExtents: [0.5, 2, 5] }, position: [5, 2, 0], kind: "static" });
54
+ const hit = backend.shapecast({
55
+ shape: { kind: "capsule", radius: 0.4, halfHeight: 0.5 },
56
+ position: [0, 1, 0],
57
+ motion: [10, 0, 0],
58
+ });
59
+ expect(hit).not.toBeNull();
60
+ expect(hit.toi).toBeGreaterThan(0.3);
61
+ expect(hit.toi).toBeLessThan(0.5);
62
+ expect(hit.normal[0]).toBeCloseTo(-1, 3);
63
+ expect(backend.shapecast({ shape: { kind: "sphere", radius: 0.4 }, position: [0, 1, 0], motion: [2, 0, 0] })).toBeNull();
64
+ });
65
+ test("overlap finds bodies at a point and nowhere else", () => {
66
+ const backend = create();
67
+ const crate = backend.addBody({ shape: { kind: "box", halfExtents: [0.5, 0.5, 0.5] }, position: [0, 0.5, 0], kind: "static" });
68
+ expect(backend.overlap({ shape: { kind: "sphere", radius: 0.2 }, position: [0.3, 0.5, 0] })).toContain(crate);
69
+ expect(backend.overlap({ shape: { kind: "sphere", radius: 0.2 }, position: [3, 0.5, 0] })).toEqual([]);
70
+ });
71
+ test("layer masks filter rays and casts", () => {
72
+ const backend = create();
73
+ backend.addBody({ shape: { kind: "box", halfExtents: [1, 1, 1] }, position: [0, 1, 0], kind: "static", layers: 2 });
74
+ expect(backend.raycast({ origin: [0, 5, 0], direction: [0, -1, 0], maxDistance: 10, mask: 1 })).toBeNull();
75
+ expect(backend.raycast({ origin: [0, 5, 0], direction: [0, -1, 0], maxDistance: 10, mask: 2 })).not.toBeNull();
76
+ });
77
+ test("a kinematic target moves the body and reports its velocity", () => {
78
+ const backend = create();
79
+ const platform = backend.addBody({ shape: { kind: "box", halfExtents: [2, 0.25, 2] }, position: [0, 1, 0], kind: "kinematic" });
80
+ backend.setKinematicTarget(platform, [0.5, 1, 0]);
81
+ backend.step(1 / 60);
82
+ const state = backend.body(platform);
83
+ expect(state.position[0]).toBeCloseTo(0.5, 3);
84
+ expect(state.velocity[0]).toBeGreaterThan(20);
85
+ });
86
+ test("contacts are reported when a body lands", () => {
87
+ const backend = create();
88
+ const ground = floor(backend);
89
+ const crate = backend.addBody({ shape: { kind: "box", halfExtents: [0.5, 0.5, 0.5] }, position: [0, 3, 0] });
90
+ const pairs = [];
91
+ backend.onContact((event) => {
92
+ pairs.push([event.a, event.b].sort((x, y) => x - y).join(":"));
93
+ });
94
+ settle(backend, 2);
95
+ expect(pairs).toContain([ground, crate].sort((x, y) => x - y).join(":"));
96
+ });
97
+ test("snapshot and restore round-trip state with stable handles", () => {
98
+ const backend = create();
99
+ floor(backend);
100
+ const crate = backend.addBody({ shape: { kind: "box", halfExtents: [0.5, 0.5, 0.5] }, position: [0, 5, 0] });
101
+ settle(backend, 0.5);
102
+ const saved = backend.snapshot();
103
+ const at = backend.body(crate).position[1];
104
+ settle(backend, 1);
105
+ expect(backend.body(crate).position[1]).not.toBe(at);
106
+ backend.restore(saved);
107
+ expect(backend.hasBody(crate)).toBe(true);
108
+ expect(backend.body(crate).position[1]).toBeCloseTo(at, 5);
109
+ const later = backend.addBody({ shape: { kind: "sphere", radius: 0.3 }, position: [2, 2, 2] });
110
+ expect(later).not.toBe(crate);
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
+ });
146
+ test("capabilities describe the backend", () => {
147
+ const backend = create();
148
+ expect(backend.capabilities.shapes.length).toBeGreaterThan(1);
149
+ expect(typeof backend.capabilities.rotation).toBe("boolean");
150
+ });
151
+ }
@@ -0,0 +1,45 @@
1
+ import { type BodyHandle, type BodyKind, type BodyShape, type JointDesc, type JointHandle, type PhysicsBackend, type PhysicsBackendConfig } from "./physicsBackend.js";
2
+ import { type PhysicsBounds, type PhysicsPrecision } from "./physicsWorld.js";
3
+ /** Construction options: the `PhysicsWorld` capacity and bounds plus the shared backend config. */
4
+ export interface PhysicsWorldBackendOptions extends PhysicsBackendConfig {
5
+ capacity: number;
6
+ bounds: PhysicsBounds;
7
+ precision?: PhysicsPrecision;
8
+ friction?: number;
9
+ restitution?: number;
10
+ /** Warn (once) when a body asks for rotation or a shape this backend approximates. Default `true`. */
11
+ warn?: boolean;
12
+ }
13
+ interface SavedBody {
14
+ handle: BodyHandle;
15
+ shape: BodyShape;
16
+ kind: BodyKind;
17
+ layers: number;
18
+ mask: number;
19
+ userData: unknown;
20
+ mass: number;
21
+ position: [number, number, number];
22
+ velocity: [number, number, number];
23
+ sleeping: boolean;
24
+ linearDamping?: number;
25
+ }
26
+ /** Serializable backend state: every live body and joint, with handles preserved. */
27
+ export interface PhysicsWorldBackendState {
28
+ nextHandle: number;
29
+ nextJoint: number;
30
+ bodies: SavedBody[];
31
+ joints: {
32
+ handle: JointHandle;
33
+ desc: JointDesc;
34
+ }[];
35
+ }
36
+ /**
37
+ * `PhysicsWorld` behind the {@link PhysicsBackend} interface: zero dependencies, translation-only. Capsules,
38
+ * convex hulls, meshes, and heightfields collide as their bounding boxes; rotation, angular velocity, CCD, and
39
+ * hinge limits are accepted and ignored (see {@link PhysicsBackend.capabilities}). Reach for `@jgengine/rapier`
40
+ * when a game needs those for real; the seam is the same.
41
+ *
42
+ * @capability physics-world-backend the zero-dependency PhysicsWorld solver exposed through the PhysicsBackend seam
43
+ */
44
+ export declare function createPhysicsWorldBackend(options: PhysicsWorldBackendOptions): PhysicsBackend;
45
+ export {};