@jgengine/core 0.11.0 → 0.13.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 (256) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat/effects.d.ts +11 -4
  9. package/dist/combat/effects.js +24 -17
  10. package/dist/combat/magazine.d.ts +13 -0
  11. package/dist/combat/magazine.js +27 -0
  12. package/dist/combat/vfxPresets.d.ts +176 -0
  13. package/dist/combat/vfxPresets.js +74 -0
  14. package/dist/combat/weaponFire.d.ts +114 -0
  15. package/dist/combat/weaponFire.js +96 -0
  16. package/dist/combat.d.ts +5 -2
  17. package/dist/combat.js +4 -1
  18. package/dist/devtools/controls.d.ts +38 -0
  19. package/dist/devtools/controls.js +181 -0
  20. package/dist/devtools/devtools.d.ts +5 -197
  21. package/dist/devtools/devtools.js +38 -774
  22. package/dist/devtools/discover.d.ts +32 -0
  23. package/dist/devtools/discover.js +240 -0
  24. package/dist/devtools/fallbackSeams.d.ts +50 -0
  25. package/dist/devtools/fallbackSeams.js +111 -0
  26. package/dist/devtools/frame.d.ts +15 -0
  27. package/dist/devtools/frame.js +247 -0
  28. package/dist/devtools/logs.d.ts +15 -0
  29. package/dist/devtools/logs.js +86 -0
  30. package/dist/devtools/overrides.d.ts +16 -0
  31. package/dist/devtools/overrides.js +66 -0
  32. package/dist/devtools/types.d.ts +212 -0
  33. package/dist/devtools/types.js +1 -0
  34. package/dist/devtools/urlFlags.d.ts +22 -0
  35. package/dist/devtools/urlFlags.js +62 -0
  36. package/dist/economy/auctionBook.d.ts +122 -0
  37. package/dist/economy/auctionBook.js +213 -0
  38. package/dist/economy/priceHistory.d.ts +44 -0
  39. package/dist/economy/priceHistory.js +89 -0
  40. package/dist/economy/unlockPoints.d.ts +92 -0
  41. package/dist/economy/unlockPoints.js +103 -0
  42. package/dist/editor/commandApply.d.ts +5 -0
  43. package/dist/editor/commandApply.js +755 -0
  44. package/dist/editor/commands.d.ts +48 -3
  45. package/dist/editor/commands.js +40 -754
  46. package/dist/editor/directives.d.ts +94 -0
  47. package/dist/editor/directives.js +206 -0
  48. package/dist/editor/document.d.ts +37 -2
  49. package/dist/editor/document.js +422 -6
  50. package/dist/editor/entityCatalog.d.ts +42 -0
  51. package/dist/editor/entityCatalog.js +101 -0
  52. package/dist/editor/environment.d.ts +74 -0
  53. package/dist/editor/environment.js +169 -0
  54. package/dist/editor/index.d.ts +26 -1
  55. package/dist/editor/index.js +8 -1
  56. package/dist/editor/minimap.d.ts +31 -0
  57. package/dist/editor/minimap.js +73 -0
  58. package/dist/editor/streamer.d.ts +66 -0
  59. package/dist/editor/streamer.js +68 -0
  60. package/dist/editor/types.d.ts +143 -0
  61. package/dist/editor/world.d.ts +136 -0
  62. package/dist/editor/world.js +303 -0
  63. package/dist/game/breeding.d.ts +161 -0
  64. package/dist/game/breeding.js +181 -0
  65. package/dist/game/cameraConfig.d.ts +451 -0
  66. package/dist/game/cameraConfig.js +1 -0
  67. package/dist/game/clipRoles.d.ts +51 -0
  68. package/dist/game/clipRoles.js +142 -0
  69. package/dist/game/defineGame.d.ts +16 -9
  70. package/dist/game/defineGame.js +14 -6
  71. package/dist/game/defineSystem.d.ts +2 -0
  72. package/dist/game/defineSystem.js +2 -0
  73. package/dist/game/events.d.ts +28 -0
  74. package/dist/game/partAnimation.d.ts +78 -0
  75. package/dist/game/partAnimation.js +147 -0
  76. package/dist/game/playableGame.d.ts +72 -440
  77. package/dist/game/playableGame.js +1 -1
  78. package/dist/game/progression.d.ts +35 -0
  79. package/dist/game/progression.js +22 -1
  80. package/dist/game/quest.js +1 -3
  81. package/dist/game/tribe.d.ts +184 -0
  82. package/dist/game/tribe.js +432 -0
  83. package/dist/gameplay.d.ts +3 -1
  84. package/dist/gameplay.js +3 -1
  85. package/dist/inventory/encumbrance.d.ts +69 -0
  86. package/dist/inventory/encumbrance.js +68 -0
  87. package/dist/inventory/inventoryModel.d.ts +24 -8
  88. package/dist/inventory/inventoryModel.js +47 -8
  89. package/dist/item/durability.js +1 -3
  90. package/dist/meta/changelog.d.ts +1 -1
  91. package/dist/meta/changelog.js +48 -1
  92. package/dist/movement/movementModel.d.ts +15 -1
  93. package/dist/movement/movementModel.js +33 -15
  94. package/dist/movement/playerMovement.js +102 -2
  95. package/dist/multiplayer/combatSnapshot.js +13 -8
  96. package/dist/multiplayer/presenceModel.d.ts +2 -0
  97. package/dist/multiplayer/presenceModel.js +8 -0
  98. package/dist/physics/drivableVehicle.d.ts +2 -0
  99. package/dist/physics/drivableVehicle.js +1 -1
  100. package/dist/physics/flightDynamics.d.ts +82 -0
  101. package/dist/physics/flightDynamics.js +165 -0
  102. package/dist/physics/gravityField.d.ts +25 -0
  103. package/dist/physics/gravityField.js +41 -0
  104. package/dist/physics/kinematicVehicle.d.ts +87 -0
  105. package/dist/physics/kinematicVehicle.js +193 -11
  106. package/dist/physics/physicsWorld.d.ts +26 -0
  107. package/dist/physics/physicsWorld.js +109 -117
  108. package/dist/physics/vehicleObstacles.d.ts +44 -0
  109. package/dist/physics/vehicleObstacles.js +115 -0
  110. package/dist/random/rng.d.ts +4 -1
  111. package/dist/random/rng.js +4 -1
  112. package/dist/runtime/adapter.d.ts +13 -0
  113. package/dist/runtime/adapter.js +13 -0
  114. package/dist/runtime/context/combat.d.ts +51 -0
  115. package/dist/runtime/context/combat.js +131 -0
  116. package/dist/runtime/context/combatFx.js +15 -5
  117. package/dist/runtime/context/deathLoot.d.ts +30 -0
  118. package/dist/runtime/context/deathLoot.js +39 -0
  119. package/dist/runtime/context/player.d.ts +48 -0
  120. package/dist/runtime/context/player.js +125 -0
  121. package/dist/runtime/context/scene.d.ts +60 -0
  122. package/dist/runtime/context/scene.js +329 -0
  123. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  124. package/dist/runtime/context/snapshotCodecs.js +26 -0
  125. package/dist/runtime/descriptors/baseline.js +26 -3
  126. package/dist/runtime/descriptors/features.d.ts +5 -0
  127. package/dist/runtime/descriptors/features.js +20 -2
  128. package/dist/runtime/gameContext.d.ts +2 -428
  129. package/dist/runtime/gameContext.js +133 -500
  130. package/dist/runtime/gameContextTypes.d.ts +500 -0
  131. package/dist/runtime/gameContextTypes.js +1 -0
  132. package/dist/runtime/headlessRunner.d.ts +17 -2
  133. package/dist/runtime/headlessRunner.js +15 -0
  134. package/dist/runtime/hostPolicy.d.ts +51 -0
  135. package/dist/runtime/hostPolicy.js +68 -0
  136. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  137. package/dist/runtime/hostedGameRunner.js +1 -0
  138. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  139. package/dist/runtime/hostedWorldSession.js +1 -0
  140. package/dist/runtime/worldSnapshot.d.ts +13 -1
  141. package/dist/runtime/worldSnapshot.js +15 -3
  142. package/dist/scene/assetCatalog.d.ts +7 -0
  143. package/dist/scene/assetSpace.js +1 -3
  144. package/dist/scene/authoredTriggers.d.ts +50 -0
  145. package/dist/scene/authoredTriggers.js +114 -0
  146. package/dist/scene/builtinSceneKinds.js +8 -3
  147. package/dist/scene/colliders.d.ts +79 -0
  148. package/dist/scene/colliders.js +170 -0
  149. package/dist/scene/collisionMesh.d.ts +94 -0
  150. package/dist/scene/collisionMesh.js +699 -0
  151. package/dist/scene/companion.d.ts +147 -0
  152. package/dist/scene/companion.js +193 -0
  153. package/dist/scene/entityStats.d.ts +7 -9
  154. package/dist/scene/entityStats.js +19 -15
  155. package/dist/scene/sceneKinds.d.ts +16 -0
  156. package/dist/scene/sceneKinds.js +4 -0
  157. package/dist/scene/sceneRaycast.js +15 -0
  158. package/dist/stats/spawnLevelStats.d.ts +110 -0
  159. package/dist/stats/spawnLevelStats.js +140 -0
  160. package/dist/stats/statModifiers.d.ts +16 -0
  161. package/dist/stats/statModifiers.js +30 -1
  162. package/dist/stats/statPool.d.ts +70 -0
  163. package/dist/stats/statPool.js +62 -0
  164. package/dist/store/changeSignal.d.ts +3 -0
  165. package/dist/store/changeSignal.js +2 -0
  166. package/dist/store/observableKeyedStore.d.ts +2 -0
  167. package/dist/store/observableKeyedStore.js +1 -0
  168. package/dist/survival/decayMeter.js +1 -3
  169. package/dist/survival/regionHealth.js +1 -3
  170. package/dist/ui/hudLayout.d.ts +5 -0
  171. package/dist/ui/hudLayout.js +17 -1
  172. package/dist/ui/panelModel.d.ts +120 -0
  173. package/dist/ui/panelModel.js +168 -0
  174. package/dist/ui.d.ts +1 -0
  175. package/dist/ui.js +1 -0
  176. package/dist/world/authoredEntities.d.ts +29 -0
  177. package/dist/world/authoredEntities.js +29 -0
  178. package/dist/world/authoredObjects.d.ts +15 -1
  179. package/dist/world/authoredObjects.js +13 -1
  180. package/dist/world/authoredSpawn.d.ts +39 -0
  181. package/dist/world/authoredSpawn.js +37 -0
  182. package/dist/world/buildSockets.d.ts +152 -0
  183. package/dist/world/buildSockets.js +188 -0
  184. package/dist/world/buildingLots.d.ts +84 -0
  185. package/dist/world/buildingLots.js +119 -0
  186. package/dist/world/buildings.d.ts +6 -0
  187. package/dist/world/cityBlocks.d.ts +152 -0
  188. package/dist/world/cityBlocks.js +670 -0
  189. package/dist/world/cityContent.d.ts +77 -0
  190. package/dist/world/cityContent.js +261 -0
  191. package/dist/world/cityGenerator.d.ts +41 -0
  192. package/dist/world/cityGenerator.js +58 -0
  193. package/dist/world/cityGeometry.d.ts +114 -0
  194. package/dist/world/cityGeometry.js +573 -0
  195. package/dist/world/cityKind.d.ts +314 -0
  196. package/dist/world/cityKind.js +1578 -0
  197. package/dist/world/environmentSummary.js +57 -1
  198. package/dist/world/features.d.ts +53 -5
  199. package/dist/world/features.js +15 -3
  200. package/dist/world/grassKind.js +2 -2
  201. package/dist/world/markers.d.ts +50 -4
  202. package/dist/world/markers.js +49 -0
  203. package/dist/world/massing.js +1 -1
  204. package/dist/world/minimapBake.d.ts +78 -0
  205. package/dist/world/minimapBake.js +208 -0
  206. package/dist/world/minimapTrack.d.ts +17 -0
  207. package/dist/world/minimapTrack.js +22 -0
  208. package/dist/world/place.d.ts +166 -0
  209. package/dist/world/place.js +96 -0
  210. package/dist/world/placeAsset.js +3 -1
  211. package/dist/world/poleLineKind.d.ts +40 -0
  212. package/dist/world/poleLineKind.js +97 -0
  213. package/dist/world/polyline.js +9 -2
  214. package/dist/world/resourceNode.d.ts +109 -0
  215. package/dist/world/resourceNode.js +140 -0
  216. package/dist/world/roads.d.ts +19 -2
  217. package/dist/world/roads.js +60 -3
  218. package/dist/world/scatter.js +2 -20
  219. package/dist/world/streetGenerator.d.ts +159 -0
  220. package/dist/world/streetGenerator.js +684 -0
  221. package/dist/world/streets.d.ts +103 -1
  222. package/dist/world/streets.js +273 -0
  223. package/dist/world/terraform.js +58 -88
  224. package/dist/world/terrain.d.ts +65 -1
  225. package/dist/world/terrain.js +119 -2
  226. package/dist/world.d.ts +14 -5
  227. package/dist/world.js +12 -3
  228. package/package.json +1 -1
  229. package/skills/game-design/SKILL.md +28 -0
  230. package/skills/game-design/agents/openai.yaml +4 -0
  231. package/skills/game-design/references/application-playbook.md +170 -0
  232. package/skills/game-design/references/field-guide.md +288 -0
  233. package/skills/jgengine/SKILL.md +8 -4
  234. package/skills/jgengine/api.md +182 -81
  235. package/skills/jgengine/capabilities.md +104 -2
  236. package/skills/jgengine/recipes/minimal-game.md +88 -0
  237. package/skills/jgengine-combat/SKILL.md +19 -1
  238. package/skills/jgengine-combat/api.md +75 -2
  239. package/skills/jgengine-combat/capabilities.md +61 -1
  240. package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
  241. package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
  242. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  243. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
  244. package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
  245. package/skills/jgengine-combat/reference.md +33 -4
  246. package/skills/jgengine-gameplay/SKILL.md +8 -0
  247. package/skills/jgengine-gameplay/api.md +182 -26
  248. package/skills/jgengine-gameplay/capabilities.md +96 -2
  249. package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
  250. package/skills/jgengine-gameplay/reference-systems.md +10 -5
  251. package/skills/jgengine-world/SKILL.md +13 -0
  252. package/skills/jgengine-world/api.md +453 -21
  253. package/skills/jgengine-world/capabilities.md +112 -2
  254. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  255. package/skills/jgengine-world/reference.md +43 -4
  256. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -2,6 +2,7 @@ import { steerYaw } from "../movement/steering.js";
2
2
  import { DEFAULT_GRIP_CURVE, sampleGripCurve } from "./vehicleBody.js";
3
3
  export function createKinematicVehicle(tuning, options = {}) {
4
4
  const grip = tuning.grip ?? DEFAULT_GRIP_CURVE;
5
+ const chassis = tuning.chassis;
5
6
  const rollingResistance = tuning.rollingResistance ?? 0;
6
7
  const surfaceFriction = options.surfaceFriction ?? (() => 1);
7
8
  const dragAt = options.dragAt ?? (() => 0);
@@ -12,6 +13,14 @@ export function createKinematicVehicle(tuning, options = {}) {
12
13
  let heading = options.heading ?? 0;
13
14
  let vx = 0;
14
15
  let vz = 0;
16
+ let gearIndex = 0;
17
+ let rpm = tuning.powertrain?.idleRpm ?? 0;
18
+ let shiftRemaining = 0;
19
+ let steerAngle = 0;
20
+ let yawRate = 0;
21
+ let previousForwardSpeed = 0;
22
+ let bodyPitch = 0;
23
+ let bodyRoll = 0;
15
24
  function forward() {
16
25
  return [Math.sin(heading), Math.cos(heading)];
17
26
  }
@@ -22,18 +31,100 @@ export function createKinematicVehicle(tuning, options = {}) {
22
31
  const turnRate = tuning.turnRate * (modifiers?.turnRateScale ?? 1);
23
32
  const [fx0, fz0] = forward();
24
33
  const speed = vx * fx0 + vz * fz0;
25
- const steerScale = Math.min(1, Math.abs(speed) / tuning.turnSpeedRef);
26
- const direction = speed >= 0 ? 1 : -1;
27
- heading = steerYaw(heading, axis.steer * steerScale * direction, turnRate, dt);
34
+ if (tuning.steering === undefined) {
35
+ const steerScale = Math.min(1, Math.abs(speed) / tuning.turnSpeedRef);
36
+ const direction = speed >= 0 ? 1 : -1;
37
+ heading = steerYaw(heading, axis.steer * steerScale * direction, turnRate, dt);
38
+ yawRate = dt > 0 ? -axis.steer * steerScale * direction * turnRate : 0;
39
+ steerAngle = axis.steer;
40
+ }
41
+ else {
42
+ const steering = tuning.steering;
43
+ const speedBlend = Math.min(1, Math.abs(speed) / Math.max(0.001, steering.highSpeedAt));
44
+ const lock = steering.maxAngle + (steering.highSpeedAngle - steering.maxAngle) * speedBlend;
45
+ const targetAngle = axis.steer * lock * (modifiers?.turnRateScale ?? 1);
46
+ const response = 1 - Math.exp(-Math.max(0, steering.response) * Math.max(0, dt));
47
+ steerAngle += (targetAngle - steerAngle) * response;
48
+ const targetYaw = Math.abs(speed) < 0.05 ? 0 : -(speed / Math.max(0.1, steering.wheelbase)) * Math.tan(steerAngle);
49
+ const yawResponse = 1 - Math.exp(-Math.max(0, steering.yawDamping ?? 10) * Math.max(0, dt));
50
+ yawRate += (targetYaw - yawRate) * yawResponse;
51
+ heading += yawRate * dt;
52
+ }
28
53
  const [fx, fz] = forward();
29
54
  let accel = 0;
30
- if (axis.throttle > 0 && speed < topSpeed)
31
- accel += axis.throttle * engineAccel;
55
+ let tractionLimited = false;
56
+ let absActive = false;
57
+ let wheelspin = false;
58
+ // Powertrain state tracks wheel speed EVERY tick (not just under throttle): the engine keeps
59
+ // spinning with the wheels, so rpm, gear and downshifts stay live while coasting or braking. That
60
+ // is what makes engine braking below gear-dependent. Drive torque itself is still applied only on
61
+ // throttle; `gearLeverage` (current ratio / top-gear ratio) is `1` in top gear and grows in the
62
+ // low gears the car downshifts into as it slows.
63
+ let torqueFactor = 1;
64
+ let gearLeverage = 1;
65
+ if (tuning.powertrain !== undefined) {
66
+ const powertrain = tuning.powertrain;
67
+ shiftRemaining = Math.max(0, shiftRemaining - dt);
68
+ const ratio = powertrain.gears[Math.min(gearIndex, powertrain.gears.length - 1)] ?? 1;
69
+ const topGearRatio = powertrain.gears[powertrain.gears.length - 1] ?? ratio;
70
+ gearLeverage = topGearRatio > 0 ? Math.min(ENGINE_BRAKE_MAX_LEVERAGE, ratio / topGearRatio) : 1;
71
+ const wheelRpm = Math.abs(speed) / Math.max(0.01, powertrain.wheelRadius) * 60 / (Math.PI * 2);
72
+ rpm = Math.max(powertrain.idleRpm, Math.min(powertrain.redlineRpm, wheelRpm * ratio * powertrain.finalDrive));
73
+ if (shiftRemaining <= 0 && rpm >= powertrain.shiftUpRpm && gearIndex < powertrain.gears.length - 1) {
74
+ gearIndex += 1;
75
+ shiftRemaining = powertrain.shiftSeconds;
76
+ }
77
+ else if (shiftRemaining <= 0 && rpm <= powertrain.shiftDownRpm && gearIndex > 0) {
78
+ gearIndex -= 1;
79
+ shiftRemaining = powertrain.shiftSeconds;
80
+ }
81
+ const normalizedRpm = (rpm - powertrain.idleRpm) / Math.max(1, powertrain.redlineRpm - powertrain.idleRpm);
82
+ const torque = sampleGripCurve(powertrain.torqueCurve, normalizedRpm);
83
+ torqueFactor = torque * (shiftRemaining > 0 ? 0.32 : 1);
84
+ }
85
+ if (axis.throttle > 0 && speed < topSpeed) {
86
+ accel += chassis === undefined
87
+ ? axis.throttle * engineAccel * torqueFactor
88
+ : (axis.throttle * chassis.engineForce * (modifiers?.accelScale ?? 1) * torqueFactor) / chassis.massKg;
89
+ }
32
90
  if (axis.brake > 0) {
33
- if (speed > 0.2)
34
- accel -= axis.brake * tuning.brakeAccel;
35
- else if (speed > -tuning.reverseSpeed)
36
- accel -= axis.brake * engineAccel;
91
+ if (speed > 0.2) {
92
+ const abs = tuning.dynamics?.abs ?? 0;
93
+ const brakeScale = Math.max(0.15, 1 - abs * Math.max(0, Math.abs(speed) / Math.max(1, topSpeed) - 0.72));
94
+ absActive = brakeScale < 0.999;
95
+ accel -= chassis === undefined
96
+ ? axis.brake * tuning.brakeAccel * brakeScale
97
+ : (axis.brake * chassis.brakeForce * brakeScale) / chassis.massKg;
98
+ }
99
+ else if (speed > -tuning.reverseSpeed) {
100
+ accel -= chassis === undefined
101
+ ? axis.brake * engineAccel
102
+ : (axis.brake * chassis.engineForce * (modifiers?.accelScale ?? 1)) / chassis.massKg;
103
+ }
104
+ }
105
+ const traction = tuning.dynamics?.tractionControl ?? 0;
106
+ if (traction > 0 && Math.abs(speed) > 0.5) {
107
+ const lateral = Math.abs(-vx * fz + vz * fx);
108
+ const excess = Math.max(0, lateral / (Math.abs(speed) + 1) - 0.18);
109
+ if (excess > 0) {
110
+ accel *= Math.max(0.2, 1 - excess * traction * 3);
111
+ tractionLimited = true;
112
+ }
113
+ }
114
+ // Coast resistance for the mass-and-force chassis (#1051) — derived from the vehicle's own weight,
115
+ // gravity, and gearing, never a flat "brake force" constant. Two weight-based terms:
116
+ // • rolling resistance F = μ_rr · m · g (always, the road-load floor)
117
+ // • engine braking F = μ_eb · m · g · gearLeverage (throttle lifted, in gear)
118
+ // Engine braking is routed through the CURRENT gear, so it strengthens as the car downshifts toward
119
+ // a stop — exactly like lifting off in a real drivetrain. Both are forces (∝ m·g) whose per-mass
120
+ // deceleration is weight-independent and gear-scaled; the per-tick decel is capped so coasting can
121
+ // never push the car backwards.
122
+ if (chassis !== undefined && dt > 0 && Math.abs(speed) > 1e-4) {
123
+ let resistDecel = ROLLING_RESISTANCE_COEFFICIENT * GRAVITY;
124
+ if (axis.throttle <= 0 && tuning.powertrain !== undefined) {
125
+ resistDecel += ENGINE_BRAKE_COEFFICIENT * GRAVITY * gearLeverage;
126
+ }
127
+ accel -= Math.sign(speed) * Math.min(resistDecel, Math.abs(speed) / dt);
37
128
  }
38
129
  vx += fx * accel * dt;
39
130
  vz += fz * accel * dt;
@@ -42,11 +133,41 @@ export function createKinematicVehicle(tuning, options = {}) {
42
133
  const surface = surfaceFriction(x, z);
43
134
  const slip = Math.abs(lateralSpeed) / (Math.abs(forwardSpeed) + 1);
44
135
  const handbrakeFactor = 1 - axis.handbrake * (1 - tuning.handbrakeGrip);
45
- const gripValue = sampleGripCurve(grip, slip) * surface * handbrakeFactor;
136
+ const downforce = 1 + (tuning.dynamics?.downforce ?? 0) * Math.min(1, Math.abs(forwardSpeed) / Math.max(1, topSpeed));
137
+ const gripValue = sampleGripCurve(grip, slip) * surface * handbrakeFactor * downforce;
46
138
  const keep = Math.max(0, 1 - gripValue * tuning.gripStrength * dt);
47
- const keptLateral = lateralSpeed * keep;
139
+ const stability = tuning.dynamics?.stabilityControl ?? 0;
140
+ let keptLateral = lateralSpeed * keep * Math.max(0, 1 - stability * Math.abs(axis.steer) * dt);
141
+ if (chassis !== undefined) {
142
+ // Friction budget (#1051): total tire force F = μeff * m * g, with μeff = tireGrip * gripValue
143
+ // (grip curve * surface * handbrake * downforce). Lateral load transfer (|centripetal accel| *
144
+ // comHeight / (trackWidth * g), capped ~0.6) shrinks the lateral share so tall/narrow chassis wash
145
+ // out earlier. Lateral consumes its capped share first; only the remainder feeds drive/brake.
146
+ const capacity = chassis.tireGrip * gripValue * chassis.massKg * GRAVITY;
147
+ const transfer = Math.min(0.6, (Math.abs(yawRate * forwardSpeed) * chassis.comHeight) / (Math.max(0.1, chassis.trackWidth) * GRAVITY));
148
+ const lateralCapacity = capacity * (1 - 0.5 * transfer);
149
+ const reductionWanted = lateralSpeed - keptLateral;
150
+ const maxReduction = dt > 0 ? (lateralCapacity * dt) / chassis.massKg : 0;
151
+ let reductionActual = reductionWanted;
152
+ if (Math.abs(reductionWanted) > maxReduction) {
153
+ reductionActual = Math.sign(reductionWanted) * maxReduction;
154
+ keptLateral = lateralSpeed - reductionActual;
155
+ }
156
+ const lateralForceUsed = dt > 0 ? (chassis.massKg * Math.abs(reductionActual)) / dt : 0;
157
+ const remainingLong = Math.max(0, capacity - lateralForceUsed);
158
+ if (chassis.massKg * Math.abs(accel) > remainingLong) {
159
+ const actualAccel = (Math.sign(accel) * remainingLong) / chassis.massKg;
160
+ forwardSpeed -= (accel - actualAccel) * dt;
161
+ if (accel > 0 && axis.throttle > 0)
162
+ wheelspin = true;
163
+ }
164
+ }
48
165
  if (rollingResistance > 0)
49
166
  forwardSpeed *= Math.max(0, 1 - rollingResistance * dt);
167
+ const aerodynamicDrag = tuning.dynamics?.aerodynamicDrag ?? 0;
168
+ if (aerodynamicDrag > 0) {
169
+ forwardSpeed *= 1 / (1 + aerodynamicDrag * Math.abs(forwardSpeed) * dt);
170
+ }
50
171
  vx = fx * forwardSpeed - fz * keptLateral;
51
172
  vz = fz * forwardSpeed + fx * keptLateral;
52
173
  const drag = dragAt(x, z);
@@ -69,6 +190,28 @@ export function createKinematicVehicle(tuning, options = {}) {
69
190
  x = toX;
70
191
  z = toZ;
71
192
  }
193
+ // Rest snap (#1051): parked with no throttle/brake/handbrake and sub-threshold motion → exact zero,
194
+ // killing residual creep and jitter. Zeroing previousForwardSpeed keeps the stop free of a pitch spike.
195
+ if (chassis !== undefined &&
196
+ axis.throttle <= 0 &&
197
+ axis.brake <= 0 &&
198
+ axis.handbrake <= 0 &&
199
+ Math.abs(vx * fx + vz * fz) < 0.2 &&
200
+ Math.abs(-vx * fz + vz * fx) < 0.2) {
201
+ vx = 0;
202
+ vz = 0;
203
+ forwardSpeed = 0;
204
+ previousForwardSpeed = 0;
205
+ }
206
+ const longitudinalAcceleration = dt > 0 ? (forwardSpeed - previousForwardSpeed) / dt : 0;
207
+ previousForwardSpeed = forwardSpeed;
208
+ const attitudeResponse = 1 - Math.exp(-Math.max(0, tuning.dynamics?.attitudeResponse ?? 7) * Math.max(0, dt));
209
+ // comHeight scales brake dive / throttle squat and cornering lean (#1051); `1` without a chassis.
210
+ const comScale = chassis !== undefined ? chassis.comHeight : 1;
211
+ const targetPitch = clampSigned(-longitudinalAcceleration * (tuning.dynamics?.bodyPitchFactor ?? 0) * comScale, tuning.dynamics?.maxBodyPitch ?? 0.12);
212
+ const targetRoll = clampSigned(-yawRate * forwardSpeed * (tuning.dynamics?.bodyRollFactor ?? 0) * comScale, tuning.dynamics?.maxBodyRoll ?? 0.16);
213
+ bodyPitch += (targetPitch - bodyPitch) * attitudeResponse;
214
+ bodyRoll += (targetRoll - bodyRoll) * attitudeResponse;
72
215
  return {
73
216
  position: [x, y, z],
74
217
  heading,
@@ -76,6 +219,16 @@ export function createKinematicVehicle(tuning, options = {}) {
76
219
  lateralSpeed: -vx * fz + vz * fx,
77
220
  slip,
78
221
  surface,
222
+ gear: speed < -0.2 ? 0 : gearIndex + 1,
223
+ rpm,
224
+ steerAngle,
225
+ yawRate,
226
+ longitudinalAcceleration,
227
+ tractionLimited,
228
+ absActive,
229
+ wheelspin,
230
+ bodyPitch,
231
+ bodyRoll,
79
232
  };
80
233
  },
81
234
  pose: () => ({ position: [x, y, z], heading }),
@@ -91,6 +244,35 @@ export function createKinematicVehicle(tuning, options = {}) {
91
244
  heading = nextHeading;
92
245
  vx = 0;
93
246
  vz = 0;
247
+ gearIndex = 0;
248
+ rpm = tuning.powertrain?.idleRpm ?? 0;
249
+ shiftRemaining = 0;
250
+ steerAngle = 0;
251
+ yawRate = 0;
252
+ previousForwardSpeed = 0;
253
+ bodyPitch = 0;
254
+ bodyRoll = 0;
94
255
  },
95
256
  };
96
257
  }
258
+ /** Gravitational acceleration, m/s², for the chassis friction budget and weight-transfer ratio (#1051). */
259
+ const GRAVITY = 9.81;
260
+ /**
261
+ * Tyre rolling-resistance coefficient μ_rr (#1051). The always-present coast force is `μ_rr · m · g`
262
+ * (the road-load floor) — a little high vs. a real ~0.013 tyre so a lifted-off car settles to a stop
263
+ * in a game-reasonable distance rather than gliding for tens of seconds.
264
+ */
265
+ const ROLLING_RESISTANCE_COEFFICIENT = 0.03;
266
+ /**
267
+ * Engine-braking drag as a weight fraction (#1051): the lift-off force is `μ_eb · m · g` routed through
268
+ * the current gear (`ratio / top-gear ratio`, capped by {@link ENGINE_BRAKE_MAX_LEVERAGE}). Like rolling
269
+ * resistance it scales with the vehicle's own weight and gravity — not a per-car constant — so every car
270
+ * settles with a consistent, firm lift-off deceleration that grows as it downshifts toward a stop, while
271
+ * always staying well short of the service brakes.
272
+ */
273
+ const ENGINE_BRAKE_COEFFICIENT = 0.11;
274
+ /** Cap on low-gear engine-braking leverage so lifting off never bites as hard as the brake pedal. */
275
+ const ENGINE_BRAKE_MAX_LEVERAGE = 2.5;
276
+ function clampSigned(value, magnitude) {
277
+ return Math.max(-Math.abs(magnitude), Math.min(Math.abs(magnitude), value));
278
+ }
@@ -199,6 +199,7 @@ export declare class PhysicsWorld {
199
199
  private jFreeCount;
200
200
  private jointHigh;
201
201
  private jointActiveCount;
202
+ private readonly anchorScratch;
202
203
  private readonly bodyFree;
203
204
  private bodyFreeCount;
204
205
  private bodyHigh;
@@ -238,6 +239,13 @@ export declare class PhysicsWorld {
238
239
  /** Move a joint's local anchor offset on A (world axes, no rotation applied by the solver). */
239
240
  setJointAnchorA(id: number, x: number, y: number, z: number): void;
240
241
  setJointRest(id: number, restLength: number): void;
242
+ /**
243
+ * Gather joint `id`'s two world anchor points into {@link anchorScratch} as [ax,ay,az,bx,by,bz]
244
+ * (A's local offset added to body A's centre, or world-origin when A is unset; B likewise, treating
245
+ * `anchorB` as a world point when B is unset). The single definition shared by {@link jointDistance},
246
+ * {@link readJointSegments}, and {@link solveJoints}.
247
+ */
248
+ private anchorWorld;
241
249
  private jointDistance;
242
250
  /** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
243
251
  readJointSegments(out: Float32Array): number;
@@ -268,10 +276,28 @@ export declare class PhysicsWorld {
268
276
  getStats(): PhysicsStats;
269
277
  private substep;
270
278
  private constrainBounds;
279
+ /**
280
+ * Walk every broadphase cell body `i`'s AABB overlaps, invoking `visit(cell)` once per cell. The
281
+ * cell-range math (six `cellCoord` calls plus the row-major triple loop) lives here so the count
282
+ * and scatter passes of {@link buildGrid} share one definition. `visit` is a caller-hoisted closure
283
+ * (created once per `buildGrid`, not per body) so the hot loop stays allocation-free.
284
+ */
285
+ private forEachBodyCell;
271
286
  private buildGrid;
272
287
  private cellOf;
273
288
  private solve;
274
289
  private resolve;
290
+ /**
291
+ * Apply an axial velocity impulse `jn` along unit axis (nX,nY,nZ) to a joint's two bodies: body A
292
+ * recoils (−) and body B advances (+), each scaled by its inverse mass. A world anchor (body index
293
+ * < 0) is skipped. Scalar args only — no allocation on the joint hot path.
294
+ */
295
+ private applyAxialImpulse;
296
+ /**
297
+ * Positional (Baumgarte) twin of {@link applyAxialImpulse}: nudge the joint's two bodies along
298
+ * (nX,nY,nZ) by `corr` — body A advances (+), body B recoils (−), each scaled by its inverse mass.
299
+ */
300
+ private applyAxialCorrection;
275
301
  private solveJoints;
276
302
  private updateSleep;
277
303
  }
@@ -95,6 +95,9 @@ export class PhysicsWorld {
95
95
  jFreeCount = 0;
96
96
  jointHigh = 0;
97
97
  jointActiveCount = 0;
98
+ // Reused scratch for a joint's two world anchor points [ax,ay,az,bx,by,bz]; filled by anchorWorld
99
+ // so the gather is defined once and no per-call array is allocated on the joint hot path.
100
+ anchorScratch = new Float64Array(6);
98
101
  // Removed bodies tombstone (FLAG_DEAD) rather than compact: `id` is the SoA slot itself, handed
99
102
  // out to callers and stored raw in joints/game state, so a slot can never move once allocated.
100
103
  // `bodyHigh` is the high-water mark of ever-used slots; `bodyFree` recycles dead ones for addBody.
@@ -313,18 +316,29 @@ export class PhysicsWorld {
313
316
  setJointRest(id, restLength) {
314
317
  this.jRest[id] = restLength;
315
318
  }
316
- jointDistance(id) {
319
+ /**
320
+ * Gather joint `id`'s two world anchor points into {@link anchorScratch} as [ax,ay,az,bx,by,bz]
321
+ * (A's local offset added to body A's centre, or world-origin when A is unset; B likewise, treating
322
+ * `anchorB` as a world point when B is unset). The single definition shared by {@link jointDistance},
323
+ * {@link readJointSegments}, and {@link solveJoints}.
324
+ */
325
+ anchorWorld(id) {
326
+ const s = this.anchorScratch;
317
327
  const bA = this.jBodyA[id];
318
328
  const bB = this.jBodyB[id];
319
- const ax = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
320
- const ay = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
321
- const az = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
322
- const bx = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
323
- const by = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
324
- const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
325
- const dx = bx - ax;
326
- const dy = by - ay;
327
- const dz = bz - az;
329
+ s[0] = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
330
+ s[1] = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
331
+ s[2] = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
332
+ s[3] = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
333
+ s[4] = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
334
+ s[5] = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
335
+ }
336
+ jointDistance(id) {
337
+ this.anchorWorld(id);
338
+ const s = this.anchorScratch;
339
+ const dx = s[3] - s[0];
340
+ const dy = s[4] - s[1];
341
+ const dz = s[5] - s[2];
328
342
  return Math.sqrt(dx * dx + dy * dy + dz * dz);
329
343
  }
330
344
  /** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
@@ -333,15 +347,15 @@ export class PhysicsWorld {
333
347
  for (let id = 0; id < this.jointHigh; id += 1) {
334
348
  if (this.jActive[id] !== 1)
335
349
  continue;
336
- const bA = this.jBodyA[id];
337
- const bB = this.jBodyB[id];
350
+ this.anchorWorld(id);
351
+ const s = this.anchorScratch;
338
352
  const o = n * 6;
339
- out[o] = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
340
- out[o + 1] = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
341
- out[o + 2] = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
342
- out[o + 3] = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
343
- out[o + 4] = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
344
- out[o + 5] = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
353
+ out[o] = s[0];
354
+ out[o + 1] = s[1];
355
+ out[o + 2] = s[2];
356
+ out[o + 3] = s[3];
357
+ out[o + 4] = s[4];
358
+ out[o + 5] = s[5];
345
359
  n += 1;
346
360
  }
347
361
  return n;
@@ -609,6 +623,29 @@ export class PhysicsWorld {
609
623
  this.velY[i] = this.velY[i] < rt ? 0 : -this.velY[i] * e;
610
624
  }
611
625
  }
626
+ /**
627
+ * Walk every broadphase cell body `i`'s AABB overlaps, invoking `visit(cell)` once per cell. The
628
+ * cell-range math (six `cellCoord` calls plus the row-major triple loop) lives here so the count
629
+ * and scatter passes of {@link buildGrid} share one definition. `visit` is a caller-hoisted closure
630
+ * (created once per `buildGrid`, not per body) so the hot loop stays allocation-free.
631
+ */
632
+ forEachBodyCell(i, visit) {
633
+ const nx = this.nx;
634
+ const ny = this.ny;
635
+ const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
636
+ const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
637
+ const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
638
+ const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
639
+ const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
640
+ const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
641
+ for (let z = z0; z <= z1; z += 1) {
642
+ for (let y = y0; y <= y1; y += 1) {
643
+ const rowBase = (z * ny + y) * nx;
644
+ for (let x = x0; x <= x1; x += 1)
645
+ visit(rowBase + x);
646
+ }
647
+ }
648
+ }
612
649
  buildGrid() {
613
650
  const n = this.bodyHigh;
614
651
  const start = this.cellStart;
@@ -618,24 +655,13 @@ export class PhysicsWorld {
618
655
  const i = this.awakeList[a];
619
656
  this.bodyCell[i] = this.cellOf(i);
620
657
  }
621
- const nx = this.nx;
622
- const ny = this.ny;
658
+ const countCell = (cell) => {
659
+ start[cell + 1] += 1;
660
+ };
623
661
  for (let i = 0; i < n; i += 1) {
624
662
  if ((this.flags[i] & FLAG_DEAD) !== 0)
625
663
  continue;
626
- const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
627
- const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
628
- const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
629
- const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
630
- const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
631
- const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
632
- for (let z = z0; z <= z1; z += 1) {
633
- for (let y = y0; y <= y1; y += 1) {
634
- const rowBase = (z * ny + y) * nx;
635
- for (let x = x0; x <= x1; x += 1)
636
- start[rowBase + x + 1] += 1;
637
- }
638
- }
664
+ this.forEachBodyCell(i, countCell);
639
665
  }
640
666
  for (let c = 0; c < this.numCells; c += 1) {
641
667
  start[c + 1] += start[c];
@@ -645,24 +671,15 @@ export class PhysicsWorld {
645
671
  if (total > this.sorted.length) {
646
672
  this.sorted = new Int32Array(Math.max(total, this.sorted.length * 2));
647
673
  }
674
+ let scatterBody = 0;
675
+ const scatterCell = (cell) => {
676
+ this.sorted[this.cursor[cell]++] = scatterBody;
677
+ };
648
678
  for (let i = 0; i < n; i += 1) {
649
679
  if ((this.flags[i] & FLAG_DEAD) !== 0)
650
680
  continue;
651
- const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
652
- const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
653
- const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
654
- const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
655
- const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
656
- const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
657
- for (let z = z0; z <= z1; z += 1) {
658
- for (let y = y0; y <= y1; y += 1) {
659
- const rowBase = (z * ny + y) * nx;
660
- for (let x = x0; x <= x1; x += 1) {
661
- const c = rowBase + x;
662
- this.sorted[this.cursor[c]++] = i;
663
- }
664
- }
665
- }
681
+ scatterBody = i;
682
+ this.forEachBodyCell(i, scatterCell);
666
683
  }
667
684
  }
668
685
  cellOf(i) {
@@ -867,6 +884,39 @@ export class PhysicsWorld {
867
884
  this.posY[j] += corr * imJ * nY;
868
885
  this.posZ[j] += corr * imJ * nZ;
869
886
  }
887
+ /**
888
+ * Apply an axial velocity impulse `jn` along unit axis (nX,nY,nZ) to a joint's two bodies: body A
889
+ * recoils (−) and body B advances (+), each scaled by its inverse mass. A world anchor (body index
890
+ * < 0) is skipped. Scalar args only — no allocation on the joint hot path.
891
+ */
892
+ applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ) {
893
+ if (bA >= 0) {
894
+ this.velX[bA] -= jn * imA * nX;
895
+ this.velY[bA] -= jn * imA * nY;
896
+ this.velZ[bA] -= jn * imA * nZ;
897
+ }
898
+ if (bB >= 0) {
899
+ this.velX[bB] += jn * imB * nX;
900
+ this.velY[bB] += jn * imB * nY;
901
+ this.velZ[bB] += jn * imB * nZ;
902
+ }
903
+ }
904
+ /**
905
+ * Positional (Baumgarte) twin of {@link applyAxialImpulse}: nudge the joint's two bodies along
906
+ * (nX,nY,nZ) by `corr` — body A advances (+), body B recoils (−), each scaled by its inverse mass.
907
+ */
908
+ applyAxialCorrection(bA, bB, imA, imB, corr, nX, nY, nZ) {
909
+ if (bA >= 0) {
910
+ this.posX[bA] += corr * imA * nX;
911
+ this.posY[bA] += corr * imA * nY;
912
+ this.posZ[bA] += corr * imA * nZ;
913
+ }
914
+ if (bB >= 0) {
915
+ this.posX[bB] -= corr * imB * nX;
916
+ this.posY[bB] -= corr * imB * nY;
917
+ this.posZ[bB] -= corr * imB * nZ;
918
+ }
919
+ }
870
920
  solveJoints(dt) {
871
921
  const beta = this.jointCorrection;
872
922
  const slop = this.slop;
@@ -876,15 +926,11 @@ export class PhysicsWorld {
876
926
  const kind = this.jKind[id];
877
927
  const bA = this.jBodyA[id];
878
928
  const bB = this.jBodyB[id];
879
- const ax = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
880
- const ay = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
881
- const az = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
882
- const bx = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
883
- const by = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
884
- const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
885
- let dx = bx - ax;
886
- let dy = by - ay;
887
- let dz = bz - az;
929
+ this.anchorWorld(id);
930
+ const s = this.anchorScratch;
931
+ let dx = s[3] - s[0];
932
+ let dy = s[4] - s[1];
933
+ let dz = s[5] - s[2];
888
934
  const imA = bA >= 0 ? this.invMass[bA] : 0;
889
935
  const imB = bB >= 0 ? this.invMass[bB] : 0;
890
936
  const imSum = imA + imB;
@@ -924,27 +970,9 @@ export class PhysicsWorld {
924
970
  }
925
971
  const rvn = rvx * nX + rvy * nY + rvz * nZ;
926
972
  const jn = -rvn / imSum;
927
- if (bA >= 0) {
928
- this.velX[bA] -= jn * imA * nX;
929
- this.velY[bA] -= jn * imA * nY;
930
- this.velZ[bA] -= jn * imA * nZ;
931
- }
932
- if (bB >= 0) {
933
- this.velX[bB] += jn * imB * nX;
934
- this.velY[bB] += jn * imB * nY;
935
- this.velZ[bB] += jn * imB * nZ;
936
- }
973
+ this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
937
974
  const corr = (dist * beta) / imSum;
938
- if (bA >= 0) {
939
- this.posX[bA] += corr * imA * nX;
940
- this.posY[bA] += corr * imA * nY;
941
- this.posZ[bA] += corr * imA * nZ;
942
- }
943
- if (bB >= 0) {
944
- this.posX[bB] -= corr * imB * nX;
945
- this.posY[bB] -= corr * imB * nY;
946
- this.posZ[bB] -= corr * imB * nZ;
947
- }
975
+ this.applyAxialCorrection(bA, bB, imA, imB, corr, nX, nY, nZ);
948
976
  }
949
977
  else {
950
978
  const rvl = Math.sqrt(rvx * rvx + rvy * rvy + rvz * rvz);
@@ -953,16 +981,7 @@ export class PhysicsWorld {
953
981
  const nY = rvy / rvl;
954
982
  const nZ = rvz / rvl;
955
983
  const jn = -rvl / imSum;
956
- if (bA >= 0) {
957
- this.velX[bA] -= jn * imA * nX;
958
- this.velY[bA] -= jn * imA * nY;
959
- this.velZ[bA] -= jn * imA * nZ;
960
- }
961
- if (bB >= 0) {
962
- this.velX[bB] += jn * imB * nX;
963
- this.velY[bB] += jn * imB * nY;
964
- this.velZ[bB] += jn * imB * nZ;
965
- }
984
+ this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
966
985
  }
967
986
  }
968
987
  continue;
@@ -994,16 +1013,7 @@ export class PhysicsWorld {
994
1013
  jn = maxJ;
995
1014
  else if (jn < -maxJ)
996
1015
  jn = -maxJ;
997
- if (bA >= 0) {
998
- this.velX[bA] -= jn * imA * nX;
999
- this.velY[bA] -= jn * imA * nY;
1000
- this.velZ[bA] -= jn * imA * nZ;
1001
- }
1002
- if (bB >= 0) {
1003
- this.velX[bB] += jn * imB * nX;
1004
- this.velY[bB] += jn * imB * nY;
1005
- this.velZ[bB] += jn * imB * nZ;
1006
- }
1016
+ this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
1007
1017
  continue;
1008
1018
  }
1009
1019
  if (kind !== JOINT_FIXED && kind !== JOINT_DISTANCE) {
@@ -1016,27 +1026,9 @@ export class PhysicsWorld {
1016
1026
  this.wake(bB);
1017
1027
  }
1018
1028
  const jn = -rvn / imSum;
1019
- if (bA >= 0) {
1020
- this.velX[bA] -= jn * imA * nX;
1021
- this.velY[bA] -= jn * imA * nY;
1022
- this.velZ[bA] -= jn * imA * nZ;
1023
- }
1024
- if (bB >= 0) {
1025
- this.velX[bB] += jn * imB * nX;
1026
- this.velY[bB] += jn * imB * nY;
1027
- this.velZ[bB] += jn * imB * nZ;
1028
- }
1029
+ this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
1029
1030
  const corr = (c * beta) / imSum;
1030
- if (bA >= 0) {
1031
- this.posX[bA] += corr * imA * nX;
1032
- this.posY[bA] += corr * imA * nY;
1033
- this.posZ[bA] += corr * imA * nZ;
1034
- }
1035
- if (bB >= 0) {
1036
- this.posX[bB] -= corr * imB * nX;
1037
- this.posY[bB] -= corr * imB * nY;
1038
- this.posZ[bB] -= corr * imB * nZ;
1039
- }
1031
+ this.applyAxialCorrection(bA, bB, imA, imB, corr, nX, nY, nZ);
1040
1032
  }
1041
1033
  }
1042
1034
  updateSleep() {