@jgengine/core 0.11.0 → 0.12.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 (219) hide show
  1. package/CHANGELOG.md +26 -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.d.ts +1 -0
  9. package/dist/combat.js +1 -0
  10. package/dist/devtools/controls.d.ts +38 -0
  11. package/dist/devtools/controls.js +181 -0
  12. package/dist/devtools/devtools.d.ts +5 -197
  13. package/dist/devtools/devtools.js +38 -774
  14. package/dist/devtools/discover.d.ts +32 -0
  15. package/dist/devtools/discover.js +240 -0
  16. package/dist/devtools/fallbackSeams.d.ts +50 -0
  17. package/dist/devtools/fallbackSeams.js +111 -0
  18. package/dist/devtools/frame.d.ts +15 -0
  19. package/dist/devtools/frame.js +247 -0
  20. package/dist/devtools/logs.d.ts +15 -0
  21. package/dist/devtools/logs.js +86 -0
  22. package/dist/devtools/overrides.d.ts +16 -0
  23. package/dist/devtools/overrides.js +66 -0
  24. package/dist/devtools/types.d.ts +212 -0
  25. package/dist/devtools/types.js +1 -0
  26. package/dist/economy/auctionBook.d.ts +122 -0
  27. package/dist/economy/auctionBook.js +213 -0
  28. package/dist/economy/priceHistory.d.ts +44 -0
  29. package/dist/economy/priceHistory.js +89 -0
  30. package/dist/economy/unlockPoints.d.ts +92 -0
  31. package/dist/economy/unlockPoints.js +103 -0
  32. package/dist/editor/commandApply.d.ts +5 -0
  33. package/dist/editor/commandApply.js +755 -0
  34. package/dist/editor/commands.d.ts +48 -3
  35. package/dist/editor/commands.js +40 -754
  36. package/dist/editor/directives.d.ts +94 -0
  37. package/dist/editor/directives.js +206 -0
  38. package/dist/editor/document.d.ts +37 -2
  39. package/dist/editor/document.js +422 -6
  40. package/dist/editor/entityCatalog.d.ts +42 -0
  41. package/dist/editor/entityCatalog.js +101 -0
  42. package/dist/editor/environment.d.ts +74 -0
  43. package/dist/editor/environment.js +169 -0
  44. package/dist/editor/index.d.ts +26 -1
  45. package/dist/editor/index.js +8 -1
  46. package/dist/editor/minimap.d.ts +31 -0
  47. package/dist/editor/minimap.js +73 -0
  48. package/dist/editor/streamer.d.ts +66 -0
  49. package/dist/editor/streamer.js +68 -0
  50. package/dist/editor/types.d.ts +143 -0
  51. package/dist/editor/world.d.ts +136 -0
  52. package/dist/editor/world.js +303 -0
  53. package/dist/game/breeding.d.ts +161 -0
  54. package/dist/game/breeding.js +181 -0
  55. package/dist/game/cameraConfig.d.ts +451 -0
  56. package/dist/game/cameraConfig.js +1 -0
  57. package/dist/game/defineGame.d.ts +16 -9
  58. package/dist/game/defineGame.js +14 -6
  59. package/dist/game/defineSystem.d.ts +2 -0
  60. package/dist/game/defineSystem.js +2 -0
  61. package/dist/game/events.d.ts +28 -0
  62. package/dist/game/playableGame.d.ts +34 -435
  63. package/dist/game/playableGame.js +1 -1
  64. package/dist/game/progression.d.ts +35 -0
  65. package/dist/game/progression.js +19 -0
  66. package/dist/game/quest.js +1 -3
  67. package/dist/game/tribe.d.ts +184 -0
  68. package/dist/game/tribe.js +432 -0
  69. package/dist/gameplay.d.ts +3 -1
  70. package/dist/gameplay.js +3 -1
  71. package/dist/inventory/encumbrance.d.ts +69 -0
  72. package/dist/inventory/encumbrance.js +68 -0
  73. package/dist/item/durability.js +1 -3
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +24 -1
  76. package/dist/movement/movementModel.d.ts +15 -1
  77. package/dist/movement/movementModel.js +33 -15
  78. package/dist/movement/playerMovement.js +102 -2
  79. package/dist/multiplayer/combatSnapshot.js +13 -8
  80. package/dist/multiplayer/presenceModel.d.ts +2 -0
  81. package/dist/multiplayer/presenceModel.js +8 -0
  82. package/dist/physics/drivableVehicle.d.ts +2 -0
  83. package/dist/physics/drivableVehicle.js +1 -1
  84. package/dist/physics/flightDynamics.d.ts +82 -0
  85. package/dist/physics/flightDynamics.js +165 -0
  86. package/dist/physics/gravityField.d.ts +25 -0
  87. package/dist/physics/gravityField.js +41 -0
  88. package/dist/physics/kinematicVehicle.d.ts +87 -0
  89. package/dist/physics/kinematicVehicle.js +193 -11
  90. package/dist/physics/physicsWorld.d.ts +26 -0
  91. package/dist/physics/physicsWorld.js +109 -117
  92. package/dist/physics/vehicleObstacles.d.ts +44 -0
  93. package/dist/physics/vehicleObstacles.js +115 -0
  94. package/dist/random/rng.d.ts +4 -1
  95. package/dist/random/rng.js +4 -1
  96. package/dist/runtime/adapter.d.ts +13 -0
  97. package/dist/runtime/adapter.js +13 -0
  98. package/dist/runtime/context/combat.d.ts +51 -0
  99. package/dist/runtime/context/combat.js +130 -0
  100. package/dist/runtime/context/deathLoot.d.ts +30 -0
  101. package/dist/runtime/context/deathLoot.js +39 -0
  102. package/dist/runtime/context/player.d.ts +48 -0
  103. package/dist/runtime/context/player.js +125 -0
  104. package/dist/runtime/context/scene.d.ts +59 -0
  105. package/dist/runtime/context/scene.js +291 -0
  106. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  107. package/dist/runtime/context/snapshotCodecs.js +26 -0
  108. package/dist/runtime/descriptors/baseline.js +26 -3
  109. package/dist/runtime/descriptors/features.d.ts +5 -0
  110. package/dist/runtime/descriptors/features.js +20 -2
  111. package/dist/runtime/gameContext.d.ts +2 -428
  112. package/dist/runtime/gameContext.js +88 -501
  113. package/dist/runtime/gameContextTypes.d.ts +467 -0
  114. package/dist/runtime/gameContextTypes.js +1 -0
  115. package/dist/runtime/headlessRunner.d.ts +4 -2
  116. package/dist/runtime/headlessRunner.js +1 -0
  117. package/dist/runtime/hostPolicy.d.ts +51 -0
  118. package/dist/runtime/hostPolicy.js +68 -0
  119. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  120. package/dist/runtime/hostedGameRunner.js +1 -0
  121. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  122. package/dist/runtime/hostedWorldSession.js +1 -0
  123. package/dist/runtime/worldSnapshot.d.ts +13 -1
  124. package/dist/runtime/worldSnapshot.js +15 -3
  125. package/dist/scene/assetCatalog.d.ts +5 -0
  126. package/dist/scene/assetSpace.js +1 -3
  127. package/dist/scene/authoredTriggers.d.ts +50 -0
  128. package/dist/scene/authoredTriggers.js +114 -0
  129. package/dist/scene/builtinSceneKinds.js +8 -3
  130. package/dist/scene/colliders.d.ts +58 -0
  131. package/dist/scene/colliders.js +108 -0
  132. package/dist/scene/collisionMesh.d.ts +94 -0
  133. package/dist/scene/collisionMesh.js +699 -0
  134. package/dist/scene/companion.d.ts +147 -0
  135. package/dist/scene/companion.js +193 -0
  136. package/dist/scene/entityStats.d.ts +7 -9
  137. package/dist/scene/entityStats.js +10 -14
  138. package/dist/scene/sceneKinds.d.ts +16 -0
  139. package/dist/scene/sceneKinds.js +4 -0
  140. package/dist/scene/sceneRaycast.js +15 -0
  141. package/dist/stats/spawnLevelStats.d.ts +110 -0
  142. package/dist/stats/spawnLevelStats.js +140 -0
  143. package/dist/stats/statPool.d.ts +70 -0
  144. package/dist/stats/statPool.js +59 -0
  145. package/dist/store/changeSignal.d.ts +3 -0
  146. package/dist/store/changeSignal.js +2 -0
  147. package/dist/store/observableKeyedStore.d.ts +2 -0
  148. package/dist/store/observableKeyedStore.js +1 -0
  149. package/dist/survival/decayMeter.js +1 -3
  150. package/dist/survival/regionHealth.js +1 -3
  151. package/dist/ui/hudLayout.d.ts +5 -0
  152. package/dist/ui/hudLayout.js +17 -1
  153. package/dist/world/authoredEntities.d.ts +29 -0
  154. package/dist/world/authoredEntities.js +29 -0
  155. package/dist/world/authoredObjects.d.ts +15 -1
  156. package/dist/world/authoredObjects.js +13 -1
  157. package/dist/world/authoredSpawn.d.ts +39 -0
  158. package/dist/world/authoredSpawn.js +37 -0
  159. package/dist/world/buildSockets.d.ts +152 -0
  160. package/dist/world/buildSockets.js +188 -0
  161. package/dist/world/cityBlocks.d.ts +152 -0
  162. package/dist/world/cityBlocks.js +670 -0
  163. package/dist/world/cityContent.d.ts +77 -0
  164. package/dist/world/cityContent.js +261 -0
  165. package/dist/world/cityGeometry.d.ts +114 -0
  166. package/dist/world/cityGeometry.js +573 -0
  167. package/dist/world/cityKind.d.ts +314 -0
  168. package/dist/world/cityKind.js +1578 -0
  169. package/dist/world/features.d.ts +18 -3
  170. package/dist/world/features.js +10 -1
  171. package/dist/world/grassKind.js +2 -2
  172. package/dist/world/markers.d.ts +50 -4
  173. package/dist/world/markers.js +49 -0
  174. package/dist/world/massing.js +1 -1
  175. package/dist/world/minimapBake.d.ts +78 -0
  176. package/dist/world/minimapBake.js +208 -0
  177. package/dist/world/minimapTrack.d.ts +17 -0
  178. package/dist/world/minimapTrack.js +22 -0
  179. package/dist/world/pathNetwork.d.ts +159 -0
  180. package/dist/world/pathNetwork.js +684 -0
  181. package/dist/world/place.d.ts +166 -0
  182. package/dist/world/place.js +96 -0
  183. package/dist/world/placeAsset.js +3 -1
  184. package/dist/world/poleLineKind.d.ts +40 -0
  185. package/dist/world/poleLineKind.js +97 -0
  186. package/dist/world/polyline.js +9 -2
  187. package/dist/world/resourceNode.d.ts +109 -0
  188. package/dist/world/resourceNode.js +140 -0
  189. package/dist/world/scatter.js +2 -20
  190. package/dist/world/streets.d.ts +29 -0
  191. package/dist/world/streets.js +55 -0
  192. package/dist/world/terraform.js +58 -88
  193. package/dist/world/terrain.d.ts +45 -0
  194. package/dist/world/terrain.js +78 -0
  195. package/dist/world.d.ts +14 -5
  196. package/dist/world.js +12 -3
  197. package/package.json +1 -1
  198. package/skills/game-design/SKILL.md +28 -0
  199. package/skills/game-design/agents/openai.yaml +4 -0
  200. package/skills/game-design/references/application-playbook.md +170 -0
  201. package/skills/game-design/references/field-guide.md +288 -0
  202. package/skills/jgengine/SKILL.md +7 -4
  203. package/skills/jgengine/api.md +172 -79
  204. package/skills/jgengine/capabilities.md +104 -2
  205. package/skills/jgengine/recipes/minimal-game.md +88 -0
  206. package/skills/jgengine-combat/SKILL.md +5 -1
  207. package/skills/jgengine-combat/api.md +36 -0
  208. package/skills/jgengine-combat/capabilities.md +44 -0
  209. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  210. package/skills/jgengine-combat/reference.md +12 -2
  211. package/skills/jgengine-gameplay/api.md +152 -26
  212. package/skills/jgengine-gameplay/capabilities.md +78 -2
  213. package/skills/jgengine-gameplay/reference-systems.md +1 -5
  214. package/skills/jgengine-world/SKILL.md +13 -0
  215. package/skills/jgengine-world/api.md +419 -15
  216. package/skills/jgengine-world/capabilities.md +108 -2
  217. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  218. package/skills/jgengine-world/reference.md +43 -4
  219. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -0,0 +1,165 @@
1
+ import { uniformGravity } from "./gravityField.js";
2
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
3
+ const length = (v) => Math.hypot(v[0], v[1], v[2]);
4
+ function axes(rotation) {
5
+ const [pitch, yaw, roll] = rotation;
6
+ const sp = Math.sin(pitch);
7
+ const cp = Math.cos(pitch);
8
+ const sy = Math.sin(yaw);
9
+ const cy = Math.cos(yaw);
10
+ const sr = Math.sin(roll);
11
+ const cr = Math.cos(roll);
12
+ const forward = [sy * cp, sp, cy * cp];
13
+ const flatRight = [cy, 0, -sy];
14
+ const flatUp = [-sy * sp, cp, -cy * sp];
15
+ return {
16
+ forward,
17
+ right: [flatRight[0] * cr + flatUp[0] * sr, flatUp[1] * sr, flatRight[2] * cr + flatUp[2] * sr],
18
+ up: [-flatRight[0] * sr + flatUp[0] * cr, flatUp[1] * cr, -flatRight[2] * sr + flatUp[2] * cr],
19
+ };
20
+ }
21
+ function dot(a, b) {
22
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
23
+ }
24
+ function wrapAngle(value) {
25
+ let angle = value;
26
+ while (angle > Math.PI)
27
+ angle -= Math.PI * 2;
28
+ while (angle < -Math.PI)
29
+ angle += Math.PI * 2;
30
+ return angle;
31
+ }
32
+ /** Six-degree-of-freedom arcade flight model for fixed-wing, helicopter, and VTOL aircraft.
33
+ * @capability flight-dynamics simulate fixed-wing, helicopter, and VTOL aircraft
34
+ */
35
+ export function createAircraftDynamics(tuning, options = {}) {
36
+ let position = [...(options.position ?? [0, 0, 0])];
37
+ let velocity = [...(options.velocity ?? [0, 0, 0])];
38
+ let rotation = [...(options.rotation ?? [0, 0, 0])];
39
+ let angular = [0, 0, 0];
40
+ let rotor = 0;
41
+ let lastVerticalSpeed = velocity[1];
42
+ const gravity = Math.abs(tuning.gravity ?? 9.81);
43
+ const gravityField = options.gravityField ?? uniformGravity([0, -gravity, 0]);
44
+ const windAt = options.wind ?? (() => [0, 0, 0]);
45
+ const groundAt = options.groundHeight ?? (() => 0);
46
+ return {
47
+ tick(dt, input) {
48
+ const stepDt = clamp(dt, 0, 0.05);
49
+ const wind = windAt(position);
50
+ const gravityVector = gravityField.sample(position);
51
+ const gravityMagnitude = Math.max(0.001, length(gravityVector));
52
+ const relative = [velocity[0] - wind[0], velocity[1] - wind[1], velocity[2] - wind[2]];
53
+ const airspeed = length(relative);
54
+ const basis = axes(rotation);
55
+ const forwardSpeed = dot(relative, basis.forward);
56
+ const rightSpeed = dot(relative, basis.right);
57
+ const upSpeed = dot(relative, basis.up);
58
+ const angleOfAttack = Math.atan2(-upSpeed, Math.max(0.01, Math.abs(forwardSpeed)));
59
+ const speedAuthority = clamp(airspeed / Math.max(1, tuning.stallSpeed), 0.12, 1);
60
+ const rotorTarget = tuning.kind === "fixedWing" ? 0 : clamp(input.collective ?? input.throttle, 0, 1);
61
+ const rotorResponse = Math.max(0.01, tuning.rotorResponse ?? 3.5);
62
+ rotor += (rotorTarget - rotor) * (1 - Math.exp(-rotorResponse * stepDt));
63
+ const vectoring = tuning.kind === "vtol" ? clamp(input.vectoring ?? 1, 0, 1) : tuning.kind === "rotorcraft" ? 1 : 0;
64
+ const transition = tuning.kind === "vtol"
65
+ ? clamp(Math.max(vectoring, 1 - airspeed / Math.max(1, tuning.vtolTransitionSpeed ?? 32)), 0, 1)
66
+ : vectoring;
67
+ const controlAuthority = transition + (1 - transition) * speedAuthority;
68
+ const rates = tuning.controls;
69
+ const targets = [
70
+ clamp(input.pitch, -1, 1) * rates.pitch * controlAuthority,
71
+ clamp(input.yaw, -1, 1) * rates.yaw * controlAuthority,
72
+ clamp(input.roll, -1, 1) * rates.roll * controlAuthority,
73
+ ];
74
+ const response = 1 - Math.exp(-Math.max(0, rates.response) * stepDt);
75
+ angular[0] += (targets[0] - angular[0]) * response;
76
+ angular[1] += (targets[1] - angular[1]) * response;
77
+ angular[2] += (targets[2] - angular[2]) * response;
78
+ const damping = Math.max(0, 1 - rates.damping * stepDt);
79
+ angular[0] *= damping;
80
+ angular[1] *= damping;
81
+ angular[2] *= damping;
82
+ const stability = clamp(rates.stability * stepDt, 0, 1);
83
+ if (Math.abs(input.pitch) < 0.05)
84
+ angular[0] += -rotation[0] * stability;
85
+ if (Math.abs(input.roll) < 0.05)
86
+ angular[2] += -rotation[2] * stability;
87
+ rotation[0] = clamp(rotation[0] + angular[0] * stepDt, -Math.PI * 0.49, Math.PI * 0.49);
88
+ rotation[1] = wrapAngle(rotation[1] + angular[1] * stepDt);
89
+ rotation[2] = wrapAngle(rotation[2] + angular[2] * stepDt);
90
+ const updated = axes(rotation);
91
+ const throttle = clamp(input.throttle, 0, 1);
92
+ const afterburner = 1 + clamp(input.afterburner ?? 0, 0, 1) * ((tuning.afterburnerMultiplier ?? 1) - 1);
93
+ const thrustAccel = tuning.maxThrust * throttle * afterburner / Math.max(0.01, tuning.mass);
94
+ const hoverAccel = (tuning.hoverThrust ?? gravity * tuning.mass * 1.08) * rotor / Math.max(0.01, tuning.mass);
95
+ const ground = groundAt(position[0], position[2]);
96
+ const clearance = Math.max(0, position[1] - ground);
97
+ const groundEffectHeight = Math.max(0.1, tuning.groundEffectHeight ?? 8);
98
+ const groundEffect = transition > 0 ? 1 + clamp(1 - clearance / groundEffectHeight, 0, 1) * 0.18 : 1;
99
+ const liftFraction = clamp((Math.abs(forwardSpeed) / Math.max(0.1, tuning.stallSpeed)) ** 2, 0, 1.35);
100
+ const stallAngle = Math.PI * 0.22;
101
+ const aoaEfficiency = clamp(1 - Math.max(0, Math.abs(angleOfAttack) - stallAngle) / (Math.PI * 0.3), 0.08, 1);
102
+ const wingLift = tuning.lift * liftFraction * aoaEfficiency * (1 - transition);
103
+ const rotorLift = hoverAccel * groundEffect * transition;
104
+ const forwardThrust = thrustAccel * (1 - transition * 0.82);
105
+ const cyclicForward = transition * hoverAccel * Math.sin(rotation[0]) * 0.75;
106
+ const cyclicSide = -transition * hoverAccel * Math.sin(rotation[2]) * 0.75;
107
+ const dragScale = tuning.drag * (1 + clamp(input.airbrake ?? 0, 0, 1) * 4);
108
+ const speedRatio = airspeed / Math.max(1, tuning.maxSpeed);
109
+ const dragAccel = dragScale * airspeed * (1 + speedRatio * speedRatio);
110
+ const invAir = airspeed > 0.001 ? 1 / airspeed : 0;
111
+ velocity[0] += (updated.forward[0] * (forwardThrust + cyclicForward) + updated.right[0] * cyclicSide + updated.up[0] * wingLift - relative[0] * invAir * dragAccel - updated.right[0] * rightSpeed * tuning.sideDrag + updated.up[0] * rotorLift + gravityVector[0]) * stepDt;
112
+ velocity[1] += (updated.forward[1] * (forwardThrust + cyclicForward) + updated.right[1] * cyclicSide + updated.up[1] * wingLift - relative[1] * invAir * dragAccel - updated.right[1] * rightSpeed * tuning.sideDrag + updated.up[1] * rotorLift + gravityVector[1]) * stepDt;
113
+ velocity[2] += (updated.forward[2] * (forwardThrust + cyclicForward) + updated.right[2] * cyclicSide + updated.up[2] * wingLift - relative[2] * invAir * dragAccel - updated.right[2] * rightSpeed * tuning.sideDrag + updated.up[2] * rotorLift + gravityVector[2]) * stepDt;
114
+ const newSpeed = length(velocity);
115
+ if (newSpeed > tuning.maxSpeed * afterburner && newSpeed > 0) {
116
+ const scale = tuning.maxSpeed * afterburner / newSpeed;
117
+ velocity[0] *= scale;
118
+ velocity[1] *= scale;
119
+ velocity[2] *= scale;
120
+ }
121
+ position[0] += velocity[0] * stepDt;
122
+ position[1] += velocity[1] * stepDt;
123
+ position[2] += velocity[2] * stepDt;
124
+ const floor = ground + (tuning.groundClearance ?? 0.6);
125
+ let grounded = false;
126
+ if (position[1] < floor) {
127
+ position[1] = floor;
128
+ if (velocity[1] < 0)
129
+ velocity[1] *= -0.08;
130
+ velocity[0] *= 0.985;
131
+ velocity[2] *= 0.985;
132
+ grounded = true;
133
+ }
134
+ const verticalAcceleration = stepDt > 0 ? (velocity[1] - lastVerticalSpeed) / stepDt : 0;
135
+ lastVerticalSpeed = velocity[1];
136
+ return {
137
+ position: [...position],
138
+ velocity: [...velocity],
139
+ rotation: [...rotation],
140
+ forwardSpeed,
141
+ airspeed,
142
+ verticalSpeed: velocity[1],
143
+ angleOfAttack,
144
+ liftFraction,
145
+ stalled: transition < 0.5 && (Math.abs(forwardSpeed) < tuning.stallSpeed || aoaEfficiency < 0.35),
146
+ grounded,
147
+ gForce: (verticalAcceleration - gravityVector[1]) / gravityMagnitude,
148
+ };
149
+ },
150
+ pose: () => ({ position: [...position], rotation: [...rotation] }),
151
+ velocity: () => [...velocity],
152
+ resetTo(nextPosition, nextRotation = [0, 0, 0]) {
153
+ position = [...nextPosition];
154
+ velocity = [0, 0, 0];
155
+ rotation = [...nextRotation];
156
+ angular = [0, 0, 0];
157
+ rotor = 0;
158
+ lastVerticalSpeed = 0;
159
+ },
160
+ setVelocity(nextVelocity) {
161
+ velocity = [...nextVelocity];
162
+ lastVerticalSpeed = velocity[1];
163
+ },
164
+ };
165
+ }
@@ -0,0 +1,25 @@
1
+ /** Three-dimensional acceleration vector sampled from a gravity field. */
2
+ export type GravityVector = readonly [number, number, number];
3
+ /** Position-dependent gravity source used by movement and vehicle simulations. */
4
+ export interface GravityField {
5
+ sample(position: GravityVector): GravityVector;
6
+ }
7
+ /** Center, strength, and falloff for radial gravity. */
8
+ export interface PointGravityOptions {
9
+ center: GravityVector;
10
+ strength: number;
11
+ falloff?: "constant" | "inverseSquare";
12
+ minRadius?: number;
13
+ }
14
+ /** Constant vector gravity for ordinary worlds, space stations, and sideways-gravity levels.
15
+ * @capability gravity-field sample configurable uniform, radial, or combined gravity
16
+ */
17
+ export declare function uniformGravity(vector?: GravityVector): GravityField;
18
+ /** Radial gravity toward one center, with optional inverse-square falloff for planetary worlds.
19
+ * @capability gravity-field sample configurable uniform, radial, or combined gravity
20
+ */
21
+ export declare function pointGravity(options: PointGravityOptions): GravityField;
22
+ /** Adds several gravity sources into one field.
23
+ * @capability gravity-field sample configurable uniform, radial, or combined gravity
24
+ */
25
+ export declare function combineGravity(fields: readonly GravityField[]): GravityField;
@@ -0,0 +1,41 @@
1
+ /** Constant vector gravity for ordinary worlds, space stations, and sideways-gravity levels.
2
+ * @capability gravity-field sample configurable uniform, radial, or combined gravity
3
+ */
4
+ export function uniformGravity(vector = [0, -9.81, 0]) {
5
+ return { sample: () => vector };
6
+ }
7
+ /** Radial gravity toward one center, with optional inverse-square falloff for planetary worlds.
8
+ * @capability gravity-field sample configurable uniform, radial, or combined gravity
9
+ */
10
+ export function pointGravity(options) {
11
+ const minRadius = Math.max(0.001, options.minRadius ?? 1);
12
+ return {
13
+ sample(position) {
14
+ const dx = options.center[0] - position[0];
15
+ const dy = options.center[1] - position[1];
16
+ const dz = options.center[2] - position[2];
17
+ const distance = Math.max(minRadius, Math.hypot(dx, dy, dz));
18
+ const scale = options.falloff === "inverseSquare" ? options.strength / (distance * distance) : options.strength;
19
+ return [dx / distance * scale, dy / distance * scale, dz / distance * scale];
20
+ },
21
+ };
22
+ }
23
+ /** Adds several gravity sources into one field.
24
+ * @capability gravity-field sample configurable uniform, radial, or combined gravity
25
+ */
26
+ export function combineGravity(fields) {
27
+ return {
28
+ sample(position) {
29
+ let x = 0;
30
+ let y = 0;
31
+ let z = 0;
32
+ for (const field of fields) {
33
+ const vector = field.sample(position);
34
+ x += vector[0];
35
+ y += vector[1];
36
+ z += vector[2];
37
+ }
38
+ return [x, y, z];
39
+ },
40
+ };
41
+ }
@@ -12,6 +12,77 @@ export interface KinematicVehicleTuning {
12
12
  handbrakeGrip: number;
13
13
  /** Forward-speed decay per second when coasting (sleds, boats); default `0`. */
14
14
  rollingResistance?: number;
15
+ /** Optional higher-fidelity powertrain. Omit to preserve the direct arcade acceleration model. */
16
+ powertrain?: KinematicPowertrainTuning;
17
+ /** Optional bicycle-model steering with speed-sensitive lock and smoothed rack response. */
18
+ steering?: KinematicSteeringTuning;
19
+ /** Optional aerodynamic drag/downforce and electronic driver assists. */
20
+ dynamics?: KinematicDynamicsTuning;
21
+ /**
22
+ * Optional mass-and-force chassis layer (#1051). When present it supersedes `engineAccel`/`brakeAccel`
23
+ * with force/mass dynamics — a tire friction budget, lateral weight transfer, and engine braking — so
24
+ * vehicles of different mass, drive/brake force, and center-of-gravity drive measurably differently.
25
+ * Omit to preserve the direct arcade acceleration model exactly.
26
+ */
27
+ chassis?: KinematicChassisTuning;
28
+ }
29
+ /**
30
+ * Mass-and-force chassis layer (#1051); when present it supersedes `engineAccel`/`brakeAccel` with
31
+ * force/mass dynamics. Drive/brake become forces divided by `massKg`, a per-tick tire friction budget
32
+ * (`tireGrip` * grip curve * surface * downforce * m * g) is split lateral-first then longitudinal so
33
+ * hard slides and launches saturate, and `comHeight`/`trackWidth` set weight-transfer washout and body
34
+ * lean. Coasting decelerates from physical road load, not a per-vehicle constant: rolling resistance
35
+ * `μ_rr · m · g` always, plus engine braking `ENGINE_BRAKE_FRACTION · engineForce` routed through the
36
+ * current gear (so it strengthens as the car downshifts) whenever the throttle is lifted and a
37
+ * `powertrain` is configured. All fields are required; omit the whole block for the legacy model.
38
+ */
39
+ export interface KinematicChassisTuning {
40
+ /** Total vehicle mass, kg (1200 compact … 11000 bus); divides every drive/brake force into accel. */
41
+ massKg: number;
42
+ /** Peak drive force at the wheels, N — scaled by the powertrain torque curve and throttle. Also sets engine-braking magnitude. */
43
+ engineForce: number;
44
+ /** Peak service-brake force, N; still modulated by the existing ABS logic. */
45
+ brakeForce: number;
46
+ /** Peak tire friction coefficient μ on an ideal surface (~0.9 street, 1.1 sport, 0.7 bus). */
47
+ tireGrip: number;
48
+ /** Center-of-mass height, m (0.45 sports car … 1.4 bus); drives weight transfer and body lean. */
49
+ comHeight: number;
50
+ /** Lateral wheel spacing (track width), m; larger resists lateral weight transfer. */
51
+ trackWidth: number;
52
+ }
53
+ /** Data-first gearbox and torque-curve tuning for a kinematic ground vehicle. */
54
+ export interface KinematicPowertrainTuning {
55
+ idleRpm: number;
56
+ redlineRpm: number;
57
+ shiftUpRpm: number;
58
+ shiftDownRpm: number;
59
+ shiftSeconds: number;
60
+ finalDrive: number;
61
+ wheelRadius: number;
62
+ gears: readonly number[];
63
+ torqueCurve: GripCurve;
64
+ }
65
+ /** Bicycle-model steering settings; all angles are radians. */
66
+ export interface KinematicSteeringTuning {
67
+ wheelbase: number;
68
+ maxAngle: number;
69
+ highSpeedAngle: number;
70
+ highSpeedAt: number;
71
+ response: number;
72
+ yawDamping?: number;
73
+ }
74
+ /** Aerodynamic and electronic-assist settings layered over tire grip. */
75
+ export interface KinematicDynamicsTuning {
76
+ aerodynamicDrag?: number;
77
+ downforce?: number;
78
+ tractionControl?: number;
79
+ abs?: number;
80
+ stabilityControl?: number;
81
+ bodyPitchFactor?: number;
82
+ bodyRollFactor?: number;
83
+ maxBodyPitch?: number;
84
+ maxBodyRoll?: number;
85
+ attitudeResponse?: number;
15
86
  }
16
87
  export interface KinematicVehicleOptions {
17
88
  position?: readonly [number, number, number];
@@ -51,6 +122,22 @@ export interface KinematicVehicleStep {
51
122
  slip: number;
52
123
  /** The `surfaceFriction` sample used this tick (`< 1` = off the ideal surface). */
53
124
  surface: number;
125
+ /** One-based forward gear, or `0` while reversing/neutral. */
126
+ gear: number;
127
+ rpm: number;
128
+ steerAngle: number;
129
+ yawRate: number;
130
+ longitudinalAcceleration: number;
131
+ tractionLimited: boolean;
132
+ absActive: boolean;
133
+ /**
134
+ * Demanded drive force exceeded the tire friction budget left after cornering this tick — a
135
+ * traction-limited launch (#1051). Always `false` without a `chassis` block; distinct from
136
+ * `tractionLimited`, which stays owned by the traction-control assist.
137
+ */
138
+ wheelspin: boolean;
139
+ bodyPitch: number;
140
+ bodyRoll: number;
54
141
  }
55
142
  /**
56
143
  * The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed,
@@ -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
  }