@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -63,6 +63,44 @@ export class ForceVolume {
63
63
  this.members = next;
64
64
  }
65
65
  }
66
+ /** One tick's force math for a body outside `PhysicsWorld` — apply the returned velocity in any custom integrator. */
67
+ export function applyVolumeForce(velocity, force, mode, dt) {
68
+ if (mode === "velocity")
69
+ return force;
70
+ if (mode === "accelerate") {
71
+ return [velocity[0] + force[0] * dt, velocity[1] + force[1] * dt, velocity[2] + force[2] * dt];
72
+ }
73
+ return [velocity[0] + force[0], velocity[1] + force[1], velocity[2] + force[2]];
74
+ }
75
+ export function createVolumeTrigger(config) {
76
+ let members = new Set();
77
+ return {
78
+ bounds: config.bounds,
79
+ step(bodies) {
80
+ const entered = [];
81
+ const insideNow = [];
82
+ const next = new Set();
83
+ for (const body of bodies) {
84
+ if (!inside(config.bounds, body.position[0], body.position[1], body.position[2]))
85
+ continue;
86
+ next.add(body.id);
87
+ insideNow.push(body.id);
88
+ if (!members.has(body.id))
89
+ entered.push(body.id);
90
+ }
91
+ const exited = [];
92
+ for (const id of members) {
93
+ if (!next.has(id))
94
+ exited.push(id);
95
+ }
96
+ members = next;
97
+ return { entered, inside: insideNow, exited };
98
+ },
99
+ reset() {
100
+ members = new Set();
101
+ },
102
+ };
103
+ }
66
104
  /**
67
105
  * Carries bodies standing on a moving platform by composing their transform with the platform's
68
106
  * per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The
@@ -0,0 +1,52 @@
1
+ import type { AxisInput } from "../input/axisInput.js";
2
+ import { type GripCurve } from "./vehicleBody.js";
3
+ export interface KinematicVehicleTuning {
4
+ engineAccel: number;
5
+ brakeAccel: number;
6
+ topSpeed: number;
7
+ reverseSpeed: number;
8
+ turnRate: number;
9
+ turnSpeedRef: number;
10
+ grip?: GripCurve;
11
+ gripStrength: number;
12
+ handbrakeGrip: number;
13
+ /** Forward-speed decay per second when coasting (sleds, boats); default `0`. */
14
+ rollingResistance?: number;
15
+ }
16
+ export interface KinematicVehicleOptions {
17
+ position?: readonly [number, number, number];
18
+ heading?: number;
19
+ /** Grip multiplier by world position — off-track gravel, ice patches; default `1` everywhere. */
20
+ surfaceFriction?: (x: number, z: number) => number;
21
+ /** Extra velocity damping per second by world position (rough ground drag); default `0`. */
22
+ dragAt?: (x: number, z: number) => number;
23
+ }
24
+ export interface KinematicVehicleStep {
25
+ position: readonly [number, number, number];
26
+ heading: number;
27
+ /** Signed speed along the heading, world units/s. */
28
+ forwardSpeed: number;
29
+ lateralSpeed: number;
30
+ /** Lateral/forward slip ratio driving the grip curve — drift indicators key off it. */
31
+ slip: number;
32
+ /** The `surfaceFriction` sample used this tick (`< 1` = off the ideal surface). */
33
+ surface: number;
34
+ }
35
+ /**
36
+ * The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed,
37
+ * throttle/brake acceleration, and a grip-curve lateral-slip bleed — no `PhysicsWorld`, no wheels,
38
+ * just the drift-friendly integration the three shipped racers proved out. Games keep their flavor
39
+ * (drift meters, boost, off-track rules) via `surfaceFriction`/`dragAt` hooks and the returned slip.
40
+ */
41
+ export interface KinematicVehicle {
42
+ tick(dt: number, axis: AxisInput): KinematicVehicleStep;
43
+ pose(): {
44
+ position: readonly [number, number, number];
45
+ heading: number;
46
+ };
47
+ velocity(): readonly [number, number];
48
+ /** Scale the current velocity (boost pads, hard collisions). */
49
+ scaleVelocity(factor: number): void;
50
+ resetTo(position: readonly [number, number, number], heading: number): void;
51
+ }
52
+ export declare function createKinematicVehicle(tuning: KinematicVehicleTuning, options?: KinematicVehicleOptions): KinematicVehicle;
@@ -0,0 +1,81 @@
1
+ import { steerYaw } from "../movement/steering.js";
2
+ import { DEFAULT_GRIP_CURVE, sampleGripCurve } from "./vehicleBody.js";
3
+ export function createKinematicVehicle(tuning, options = {}) {
4
+ const grip = tuning.grip ?? DEFAULT_GRIP_CURVE;
5
+ const rollingResistance = tuning.rollingResistance ?? 0;
6
+ const surfaceFriction = options.surfaceFriction ?? (() => 1);
7
+ const dragAt = options.dragAt ?? (() => 0);
8
+ let x = options.position?.[0] ?? 0;
9
+ let y = options.position?.[1] ?? 0;
10
+ let z = options.position?.[2] ?? 0;
11
+ let heading = options.heading ?? 0;
12
+ let vx = 0;
13
+ let vz = 0;
14
+ function forward() {
15
+ return [Math.sin(heading), Math.cos(heading)];
16
+ }
17
+ return {
18
+ tick(dt, axis) {
19
+ const [fx0, fz0] = forward();
20
+ const speed = vx * fx0 + vz * fz0;
21
+ const steerScale = Math.min(1, Math.abs(speed) / tuning.turnSpeedRef);
22
+ const direction = speed >= 0 ? 1 : -1;
23
+ heading = steerYaw(heading, axis.steer * steerScale * direction, tuning.turnRate, dt);
24
+ const [fx, fz] = forward();
25
+ let accel = 0;
26
+ if (axis.throttle > 0 && speed < tuning.topSpeed)
27
+ accel += axis.throttle * tuning.engineAccel;
28
+ if (axis.brake > 0) {
29
+ if (speed > 0.2)
30
+ accel -= axis.brake * tuning.brakeAccel;
31
+ else if (speed > -tuning.reverseSpeed)
32
+ accel -= axis.brake * tuning.engineAccel;
33
+ }
34
+ vx += fx * accel * dt;
35
+ vz += fz * accel * dt;
36
+ let forwardSpeed = vx * fx + vz * fz;
37
+ const lateralSpeed = -vx * fz + vz * fx;
38
+ const surface = surfaceFriction(x, z);
39
+ const slip = Math.abs(lateralSpeed) / (Math.abs(forwardSpeed) + 1);
40
+ const handbrakeFactor = 1 - axis.handbrake * (1 - tuning.handbrakeGrip);
41
+ const gripValue = sampleGripCurve(grip, slip) * surface * handbrakeFactor;
42
+ const keep = Math.max(0, 1 - gripValue * tuning.gripStrength * dt);
43
+ const keptLateral = lateralSpeed * keep;
44
+ if (rollingResistance > 0)
45
+ forwardSpeed *= Math.max(0, 1 - rollingResistance * dt);
46
+ vx = fx * forwardSpeed - fz * keptLateral;
47
+ vz = fz * forwardSpeed + fx * keptLateral;
48
+ const drag = dragAt(x, z);
49
+ let dragDamp = 1;
50
+ if (drag > 0) {
51
+ dragDamp = Math.max(0, 1 - drag * dt);
52
+ vx *= dragDamp;
53
+ vz *= dragDamp;
54
+ }
55
+ x += vx * dt;
56
+ z += vz * dt;
57
+ return {
58
+ position: [x, y, z],
59
+ heading,
60
+ forwardSpeed: vx * fx + vz * fz,
61
+ lateralSpeed: keptLateral * dragDamp,
62
+ slip,
63
+ surface,
64
+ };
65
+ },
66
+ pose: () => ({ position: [x, y, z], heading }),
67
+ velocity: () => [vx, vz],
68
+ scaleVelocity(factor) {
69
+ vx *= factor;
70
+ vz *= factor;
71
+ },
72
+ resetTo(position, nextHeading) {
73
+ x = position[0];
74
+ y = position[1];
75
+ z = position[2];
76
+ heading = nextHeading;
77
+ vx = 0;
78
+ vz = 0;
79
+ },
80
+ };
81
+ }
@@ -72,11 +72,15 @@ export type AddBodyOptions = BoxBodyOptions | SphereBodyOptions;
72
72
  export declare const SHAPE_BOX = 0;
73
73
  export declare const SHAPE_SPHERE = 1;
74
74
  /**
75
- * `hinge`/`fixed`/`distance` are hard bilateral constraints; `spring` is a soft PD constraint.
76
- * No angular state → `hinge`/`fixed` both pin the shared anchor (`axis` is metadata), `distance`
77
- * holds a fixed separation, `spring` drives toward `restLength` (suspension, follow-point carry).
75
+ * Hard bilateral: `fixed` pins the shared anchor (3-DOF), `hinge` pins only the plane
76
+ * perpendicular to `axis` (free along the axis — translational cylindrical), `distance`
77
+ * holds a fixed separation. `spring` is a soft PD drive toward `restLength`.
78
78
  */
79
79
  export type JointKind = "hinge" | "distance" | "spring" | "fixed";
80
+ export declare const JOINT_FIXED = 0;
81
+ export declare const JOINT_HINGE = 1;
82
+ export declare const JOINT_DISTANCE = 2;
83
+ export declare const JOINT_SPRING = 3;
80
84
  export interface JointOptions {
81
85
  /** First body index. */
82
86
  bodyA: number;
@@ -94,7 +98,7 @@ export interface JointOptions {
94
98
  damping?: number;
95
99
  /** Clamp on the per-substep spring impulse (a follow-point that never yanks). Default unbounded. */
96
100
  maxImpulse?: number;
97
- /** Hinge axis, retained as metadata (this sim has no angular DOF). */
101
+ /** Unit hinge axis (world). Required semantics for `hingeJoint`; free motion along this axis. Default [0,1,0]. */
98
102
  axis?: readonly [number, number, number];
99
103
  }
100
104
  /** A contact reported to `onCollision`. The object is reused each call — read/copy, never retain. */
@@ -157,12 +161,12 @@ export declare class PhysicsWorld {
157
161
  private readonly numCells;
158
162
  private readonly cellStart;
159
163
  private readonly cursor;
160
- private readonly sorted;
164
+ private sorted;
161
165
  private readonly bodyCell;
162
166
  private readonly awakeList;
163
167
  private readonly awakeSlot;
164
168
  private awakeCount;
165
- private readonly jSpring;
169
+ private readonly jKind;
166
170
  private readonly jActive;
167
171
  private readonly jBodyA;
168
172
  private readonly jBodyB;
@@ -172,6 +176,9 @@ export declare class PhysicsWorld {
172
176
  private readonly jAnchorBX;
173
177
  private readonly jAnchorBY;
174
178
  private readonly jAnchorBZ;
179
+ private readonly jAxisX;
180
+ private readonly jAxisY;
181
+ private readonly jAxisZ;
175
182
  private readonly jRest;
176
183
  private readonly jStiffness;
177
184
  private readonly jDamping;
@@ -1,5 +1,9 @@
1
1
  export const SHAPE_BOX = 0;
2
2
  export const SHAPE_SPHERE = 1;
3
+ export const JOINT_FIXED = 0;
4
+ export const JOINT_HINGE = 1;
5
+ export const JOINT_DISTANCE = 2;
6
+ export const JOINT_SPRING = 3;
3
7
  const FLAG_SLEEPING = 1;
4
8
  const FLAG_STATIC = 2;
5
9
  const FLAG_DEAD = 4;
@@ -65,7 +69,7 @@ export class PhysicsWorld {
65
69
  awakeList;
66
70
  awakeSlot;
67
71
  awakeCount = 0;
68
- jSpring;
72
+ jKind;
69
73
  jActive;
70
74
  jBodyA;
71
75
  jBodyB;
@@ -75,6 +79,9 @@ export class PhysicsWorld {
75
79
  jAnchorBX;
76
80
  jAnchorBY;
77
81
  jAnchorBZ;
82
+ jAxisX;
83
+ jAxisY;
84
+ jAxisZ;
78
85
  jRest;
79
86
  jStiffness;
80
87
  jDamping;
@@ -168,7 +175,7 @@ export class PhysicsWorld {
168
175
  this.awakeSlot = new Int32Array(cap).fill(-1);
169
176
  this.bodyFree = new Int32Array(cap);
170
177
  const jc = this.jointCapacity;
171
- this.jSpring = new Uint8Array(jc);
178
+ this.jKind = new Uint8Array(jc);
172
179
  this.jActive = new Uint8Array(jc);
173
180
  this.jBodyA = new Int32Array(jc);
174
181
  this.jBodyB = new Int32Array(jc);
@@ -178,6 +185,9 @@ export class PhysicsWorld {
178
185
  this.jAnchorBX = new Float32Array(jc);
179
186
  this.jAnchorBY = new Float32Array(jc);
180
187
  this.jAnchorBZ = new Float32Array(jc);
188
+ this.jAxisX = new Float32Array(jc);
189
+ this.jAxisY = new Float32Array(jc);
190
+ this.jAxisZ = new Float32Array(jc);
181
191
  this.jRest = new Float32Array(jc);
182
192
  this.jStiffness = new Float32Array(jc);
183
193
  this.jDamping = new Float32Array(jc);
@@ -224,18 +234,25 @@ export class PhysicsWorld {
224
234
  this.collisionMinSpeed = minApproachSpeed;
225
235
  }
226
236
  hingeJoint(options) {
227
- return this.addJoint(options, false, 0);
237
+ const raw = options.axis ?? [0, 1, 0];
238
+ const len = Math.hypot(raw[0], raw[1], raw[2]);
239
+ if (!(len > 1e-9))
240
+ throw new Error("PhysicsWorld: hingeJoint axis must be non-zero");
241
+ return this.addJoint(options, JOINT_HINGE, 0, [raw[0] / len, raw[1] / len, raw[2] / len]);
228
242
  }
229
243
  fixedJoint(options) {
230
- return this.addJoint(options, false, 0);
244
+ return this.addJoint(options, JOINT_FIXED, 0, null);
231
245
  }
232
246
  distanceJoint(options) {
233
- return this.addJoint(options, false, null);
247
+ return this.addJoint(options, JOINT_DISTANCE, null, null);
234
248
  }
235
249
  springJoint(options) {
236
- return this.addJoint(options, true, null);
250
+ return this.addJoint(options, JOINT_SPRING, null, null);
237
251
  }
238
- addJoint(options, spring, restOverride) {
252
+ addJoint(options, kind, restOverride, axis) {
253
+ if (kind !== JOINT_FIXED && kind !== JOINT_HINGE && kind !== JOINT_DISTANCE && kind !== JOINT_SPRING) {
254
+ throw new Error(`PhysicsWorld: joint kind ${kind} is not implemented`);
255
+ }
239
256
  let id;
240
257
  if (this.jFreeCount > 0) {
241
258
  id = this.jFree[--this.jFreeCount];
@@ -256,7 +273,10 @@ export class PhysicsWorld {
256
273
  this.jAnchorBX[id] = aB?.[0] ?? 0;
257
274
  this.jAnchorBY[id] = aB?.[1] ?? 0;
258
275
  this.jAnchorBZ[id] = aB?.[2] ?? 0;
259
- this.jSpring[id] = spring ? 1 : 0;
276
+ this.jKind[id] = kind;
277
+ this.jAxisX[id] = axis?.[0] ?? 0;
278
+ this.jAxisY[id] = axis?.[1] ?? 0;
279
+ this.jAxisZ[id] = axis?.[2] ?? 0;
260
280
  this.jStiffness[id] = options.stiffness ?? 40;
261
281
  this.jDamping[id] = options.damping ?? 6;
262
282
  this.jMaxImpulse[id] = options.maxImpulse ?? Number.POSITIVE_INFINITY;
@@ -587,28 +607,56 @@ export class PhysicsWorld {
587
607
  const n = this.bodyHigh;
588
608
  const start = this.cellStart;
589
609
  start.fill(0);
590
- // Sleeping bodies never move, so their cell index is cached from addBody / their last awake
591
- // substep — only awake bodies pay the cell recomputation each substep.
592
610
  const awakeCount = this.awakeCount;
593
611
  for (let a = 0; a < awakeCount; a += 1) {
594
612
  const i = this.awakeList[a];
595
613
  this.bodyCell[i] = this.cellOf(i);
596
614
  }
597
- // Dead slots (tombstoned by removeBody) sit inside [0, bodyHigh) but never enter the grid.
615
+ const nx = this.nx;
616
+ const ny = this.ny;
598
617
  for (let i = 0; i < n; i += 1) {
599
618
  if ((this.flags[i] & FLAG_DEAD) !== 0)
600
619
  continue;
601
- start[this.bodyCell[i] + 1] += 1;
620
+ const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
621
+ const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
622
+ const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
623
+ const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
624
+ const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
625
+ const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
626
+ for (let z = z0; z <= z1; z += 1) {
627
+ for (let y = y0; y <= y1; y += 1) {
628
+ const rowBase = (z * ny + y) * nx;
629
+ for (let x = x0; x <= x1; x += 1)
630
+ start[rowBase + x + 1] += 1;
631
+ }
632
+ }
602
633
  }
603
634
  for (let c = 0; c < this.numCells; c += 1) {
604
635
  start[c + 1] += start[c];
605
636
  this.cursor[c] = start[c];
606
637
  }
638
+ const total = start[this.numCells];
639
+ if (total > this.sorted.length) {
640
+ this.sorted = new Int32Array(Math.max(total, this.sorted.length * 2));
641
+ }
607
642
  for (let i = 0; i < n; i += 1) {
608
643
  if ((this.flags[i] & FLAG_DEAD) !== 0)
609
644
  continue;
610
- const c = this.bodyCell[i];
611
- this.sorted[this.cursor[c]++] = i;
645
+ const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
646
+ const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
647
+ const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
648
+ const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
649
+ const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
650
+ const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
651
+ for (let z = z0; z <= z1; z += 1) {
652
+ for (let y = y0; y <= y1; y += 1) {
653
+ const rowBase = (z * ny + y) * nx;
654
+ for (let x = x0; x <= x1; x += 1) {
655
+ const c = rowBase + x;
656
+ this.sorted[this.cursor[c]++] = i;
657
+ }
658
+ }
659
+ }
612
660
  }
613
661
  }
614
662
  cellOf(i) {
@@ -620,26 +668,18 @@ export class PhysicsWorld {
620
668
  solve(restitutionPass, countPass) {
621
669
  const nx = this.nx;
622
670
  const ny = this.ny;
623
- const nz = this.nz;
624
- const nxny = nx * ny;
625
- // A contact may wake a sleeping body mid-pass, appending it to the awake list; re-read the
626
- // count each iteration so it is solved this substep too.
627
671
  for (let a = 0; a < this.awakeCount; a += 1) {
628
672
  const i = this.awakeList[a];
629
- const c = this.bodyCell[i];
630
- const cz = (c / nxny) | 0;
631
- const cy = ((c - cz * nxny) / nx) | 0;
632
- const cx = c - cz * nxny - cy * nx;
633
- const z0 = cz > 0 ? cz - 1 : cz;
634
- const z1 = cz < nz - 1 ? cz + 1 : cz;
635
- const y0 = cy > 0 ? cy - 1 : cy;
636
- const y1 = cy < ny - 1 ? cy + 1 : cy;
637
- const x0 = cx > 0 ? cx - 1 : cx;
638
- const x1 = cx < nx - 1 ? cx + 1 : cx;
639
- for (let z = z0; z <= z1; z += 1) {
640
- for (let y = y0; y <= y1; y += 1) {
673
+ const ix0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
674
+ const ix1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
675
+ const iy0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
676
+ const iy1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
677
+ const iz0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
678
+ const iz1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
679
+ for (let z = iz0; z <= iz1; z += 1) {
680
+ for (let y = iy0; y <= iy1; y += 1) {
641
681
  const rowBase = (z * ny + y) * nx;
642
- for (let x = x0; x <= x1; x += 1) {
682
+ for (let x = ix0; x <= ix1; x += 1) {
643
683
  const cell = rowBase + x;
644
684
  const end = this.cellStart[cell + 1];
645
685
  for (let s = this.cellStart[cell]; s < end; s += 1) {
@@ -649,6 +689,14 @@ export class PhysicsWorld {
649
689
  const jSleeping = (this.flags[j] & FLAG_SLEEPING) !== 0;
650
690
  if (!jSleeping && j < i)
651
691
  continue;
692
+ const jx0 = cellCoord(this.posX[j] - this.halfX[j], this.bounds.min[0], this.cellSize, this.nx);
693
+ const jy0 = cellCoord(this.posY[j] - this.halfY[j], this.bounds.min[1], this.cellSize, this.ny);
694
+ const jz0 = cellCoord(this.posZ[j] - this.halfZ[j], this.bounds.min[2], this.cellSize, this.nz);
695
+ const mx0 = ix0 > jx0 ? ix0 : jx0;
696
+ const my0 = iy0 > jy0 ? iy0 : jy0;
697
+ const mz0 = iz0 > jz0 ? iz0 : jz0;
698
+ if (x !== mx0 || y !== my0 || z !== mz0)
699
+ continue;
652
700
  this.resolve(i, j, restitutionPass, countPass);
653
701
  }
654
702
  }
@@ -819,6 +867,7 @@ export class PhysicsWorld {
819
867
  for (let id = 0; id < this.jointHigh; id += 1) {
820
868
  if (this.jActive[id] !== 1)
821
869
  continue;
870
+ const kind = this.jKind[id];
822
871
  const bA = this.jBodyA[id];
823
872
  const bB = this.jBodyB[id];
824
873
  const ax = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
@@ -827,9 +876,91 @@ export class PhysicsWorld {
827
876
  const bx = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
828
877
  const by = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
829
878
  const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
830
- const dx = bx - ax;
831
- const dy = by - ay;
832
- const dz = bz - az;
879
+ let dx = bx - ax;
880
+ let dy = by - ay;
881
+ let dz = bz - az;
882
+ const imA = bA >= 0 ? this.invMass[bA] : 0;
883
+ const imB = bB >= 0 ? this.invMass[bB] : 0;
884
+ const imSum = imA + imB;
885
+ if (imSum === 0)
886
+ continue;
887
+ if (kind === JOINT_HINGE) {
888
+ const ux = this.jAxisX[id];
889
+ const uy = this.jAxisY[id];
890
+ const uz = this.jAxisZ[id];
891
+ const along = dx * ux + dy * uy + dz * uz;
892
+ dx -= along * ux;
893
+ dy -= along * uy;
894
+ dz -= along * uz;
895
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
896
+ const vax = bA >= 0 ? this.velX[bA] : 0;
897
+ const vay = bA >= 0 ? this.velY[bA] : 0;
898
+ const vaz = bA >= 0 ? this.velZ[bA] : 0;
899
+ const vbx = bB >= 0 ? this.velX[bB] : 0;
900
+ const vby = bB >= 0 ? this.velY[bB] : 0;
901
+ const vbz = bB >= 0 ? this.velZ[bB] : 0;
902
+ let rvx = vbx - vax;
903
+ let rvy = vby - vay;
904
+ let rvz = vbz - vaz;
905
+ const rvAlong = rvx * ux + rvy * uy + rvz * uz;
906
+ rvx -= rvAlong * ux;
907
+ rvy -= rvAlong * uy;
908
+ rvz -= rvAlong * uz;
909
+ if (dist > 1e-9) {
910
+ const nX = dx / dist;
911
+ const nY = dy / dist;
912
+ const nZ = dz / dist;
913
+ if (dist > slop) {
914
+ if (bA >= 0)
915
+ this.wake(bA);
916
+ if (bB >= 0)
917
+ this.wake(bB);
918
+ }
919
+ const rvn = rvx * nX + rvy * nY + rvz * nZ;
920
+ const jn = -rvn / imSum;
921
+ if (bA >= 0) {
922
+ this.velX[bA] -= jn * imA * nX;
923
+ this.velY[bA] -= jn * imA * nY;
924
+ this.velZ[bA] -= jn * imA * nZ;
925
+ }
926
+ if (bB >= 0) {
927
+ this.velX[bB] += jn * imB * nX;
928
+ this.velY[bB] += jn * imB * nY;
929
+ this.velZ[bB] += jn * imB * nZ;
930
+ }
931
+ const corr = (dist * beta) / imSum;
932
+ if (bA >= 0) {
933
+ this.posX[bA] += corr * imA * nX;
934
+ this.posY[bA] += corr * imA * nY;
935
+ this.posZ[bA] += corr * imA * nZ;
936
+ }
937
+ if (bB >= 0) {
938
+ this.posX[bB] -= corr * imB * nX;
939
+ this.posY[bB] -= corr * imB * nY;
940
+ this.posZ[bB] -= corr * imB * nZ;
941
+ }
942
+ }
943
+ else {
944
+ const rvl = Math.sqrt(rvx * rvx + rvy * rvy + rvz * rvz);
945
+ if (rvl > 1e-9) {
946
+ const nX = rvx / rvl;
947
+ const nY = rvy / rvl;
948
+ const nZ = rvz / rvl;
949
+ const jn = -rvl / imSum;
950
+ if (bA >= 0) {
951
+ this.velX[bA] -= jn * imA * nX;
952
+ this.velY[bA] -= jn * imA * nY;
953
+ this.velZ[bA] -= jn * imA * nZ;
954
+ }
955
+ if (bB >= 0) {
956
+ this.velX[bB] += jn * imB * nX;
957
+ this.velY[bB] += jn * imB * nY;
958
+ this.velZ[bB] += jn * imB * nZ;
959
+ }
960
+ }
961
+ }
962
+ continue;
963
+ }
833
964
  const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
834
965
  if (dist < 1e-9)
835
966
  continue;
@@ -837,11 +968,6 @@ export class PhysicsWorld {
837
968
  const nY = dy / dist;
838
969
  const nZ = dz / dist;
839
970
  const c = dist - this.jRest[id];
840
- const imA = bA >= 0 ? this.invMass[bA] : 0;
841
- const imB = bB >= 0 ? this.invMass[bB] : 0;
842
- const imSum = imA + imB;
843
- if (imSum === 0)
844
- continue;
845
971
  const vax = bA >= 0 ? this.velX[bA] : 0;
846
972
  const vay = bA >= 0 ? this.velY[bA] : 0;
847
973
  const vaz = bA >= 0 ? this.velZ[bA] : 0;
@@ -849,7 +975,7 @@ export class PhysicsWorld {
849
975
  const vby = bB >= 0 ? this.velY[bB] : 0;
850
976
  const vbz = bB >= 0 ? this.velZ[bB] : 0;
851
977
  const rvn = (vbx - vax) * nX + (vby - vay) * nY + (vbz - vaz) * nZ;
852
- if (this.jSpring[id] === 1) {
978
+ if (kind === JOINT_SPRING) {
853
979
  if (Math.abs(c) > slop) {
854
980
  if (bA >= 0)
855
981
  this.wake(bA);
@@ -874,6 +1000,9 @@ export class PhysicsWorld {
874
1000
  }
875
1001
  continue;
876
1002
  }
1003
+ if (kind !== JOINT_FIXED && kind !== JOINT_DISTANCE) {
1004
+ throw new Error(`PhysicsWorld: joint kind ${kind} is not implemented`);
1005
+ }
877
1006
  if (Math.abs(c) > slop) {
878
1007
  if (bA >= 0)
879
1008
  this.wake(bA);
@@ -88,7 +88,7 @@ export function createRagdoll(world, config) {
88
88
  (world.posY[a] + world.posY[b]) / 2,
89
89
  (world.posZ[a] + world.posZ[b]) / 2,
90
90
  ];
91
- links.push(world.hingeJoint({
91
+ links.push(world.fixedJoint({
92
92
  bodyA: a,
93
93
  bodyB: b,
94
94
  anchorA: [mid[0] - world.posX[a], mid[1] - world.posY[a], mid[2] - world.posZ[a]],
@@ -1,3 +1,4 @@
1
+ import { steerYaw } from "../movement/steering.js";
1
2
  /**
2
3
  * Piecewise-linear tire-grip curve: normalized lateral slip → available grip (0..1). Grip peaks near
3
4
  * the breakaway slip then falls off as the tire slides — the shape that separates a planted corner
@@ -166,7 +167,7 @@ export class VehicleBody {
166
167
  if (grounded) {
167
168
  const steerScale = Math.min(1, Math.abs(speed) / this.turnSpeedRef);
168
169
  const dir = speed >= 0 ? 1 : -1;
169
- this.heading += input.steer * this.turnRate * steerScale * dir * dt;
170
+ this.heading = steerYaw(this.heading, input.steer * steerScale * dir, this.turnRate, dt);
170
171
  }
171
172
  const [fx, fz] = this.forward;
172
173
  if (grounded) {
@@ -1,2 +1,4 @@
1
+ /** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence. */
1
2
  export declare function seededRng(seed: string | number): () => number;
3
+ /** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
2
4
  export declare function seededStreams(seed: string | number): (stream: string) => () => number;
@@ -7,6 +7,7 @@ function hashSeed(seed) {
7
7
  }
8
8
  return h >>> 0;
9
9
  }
10
+ /** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence. */
10
11
  export function seededRng(seed) {
11
12
  let a = hashSeed(seed) >>> 0;
12
13
  return () => {
@@ -17,6 +18,7 @@ export function seededRng(seed) {
17
18
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
18
19
  };
19
20
  }
21
+ /** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
20
22
  export function seededStreams(seed) {
21
23
  const base = typeof seed === "number" ? seed.toString() : seed;
22
24
  return (stream) => seededRng(`${base}:${stream}`);
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_SEED_PARAM = "seed";
2
+ export declare function withSeedParam(url: string, seed: string | number, param?: string): string;
3
+ export declare function seedFromSearch(search: string, param?: string): string | null;
4
+ export declare function seedFromUrl(url: string, param?: string): string | null;
5
+ export declare function dailySeed(nowMs: number, salt?: string): string;
@@ -0,0 +1,35 @@
1
+ export const DEFAULT_SEED_PARAM = "seed";
2
+ function splitUrl(url) {
3
+ const hashIndex = url.indexOf("#");
4
+ const hash = hashIndex === -1 ? "" : url.slice(hashIndex + 1);
5
+ const rest = hashIndex === -1 ? url : url.slice(0, hashIndex);
6
+ const queryIndex = rest.indexOf("?");
7
+ const base = queryIndex === -1 ? rest : rest.slice(0, queryIndex);
8
+ const query = queryIndex === -1 ? "" : rest.slice(queryIndex + 1);
9
+ return { base, query, hash };
10
+ }
11
+ export function withSeedParam(url, seed, param = DEFAULT_SEED_PARAM) {
12
+ const { base, query, hash } = splitUrl(url);
13
+ const params = new URLSearchParams(query);
14
+ params.set(param, typeof seed === "number" ? seed.toString() : seed);
15
+ const queryString = params.toString();
16
+ return `${base}${queryString ? `?${queryString}` : ""}${hash ? `#${hash}` : ""}`;
17
+ }
18
+ export function seedFromSearch(search, param = DEFAULT_SEED_PARAM) {
19
+ const normalized = search.startsWith("?") ? search.slice(1) : search;
20
+ const params = new URLSearchParams(normalized);
21
+ const value = params.get(param);
22
+ return value === null || value === "" ? null : value;
23
+ }
24
+ export function seedFromUrl(url, param = DEFAULT_SEED_PARAM) {
25
+ const { query } = splitUrl(url);
26
+ return seedFromSearch(query, param);
27
+ }
28
+ export function dailySeed(nowMs, salt) {
29
+ const date = new Date(nowMs);
30
+ const year = date.getUTCFullYear().toString().padStart(4, "0");
31
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
32
+ const day = date.getUTCDate().toString().padStart(2, "0");
33
+ const base = `${year}-${month}-${day}`;
34
+ return salt !== undefined && salt.length > 0 ? `${base}:${salt}` : base;
35
+ }