@jgengine/core 0.7.0 → 0.9.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 (329) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +2 -0
  3. package/dist/ai/flock.d.ts +36 -0
  4. package/dist/ai/flock.js +182 -0
  5. package/dist/ai/groupAssist.d.ts +19 -0
  6. package/dist/ai/groupAssist.js +59 -0
  7. package/dist/ai/laneSelect.d.ts +21 -0
  8. package/dist/ai/laneSelect.js +37 -0
  9. package/dist/ai/mobBrain.d.ts +68 -0
  10. package/dist/ai/mobBrain.js +115 -0
  11. package/dist/ai/spawnDirector.d.ts +5 -0
  12. package/dist/ai/spawnDirector.js +14 -2
  13. package/dist/ai/threat.d.ts +2 -0
  14. package/dist/ai/threat.js +34 -0
  15. package/dist/anim/easing.d.ts +14 -0
  16. package/dist/anim/easing.js +55 -0
  17. package/dist/behaviour/behaviour.d.ts +136 -0
  18. package/dist/behaviour/behaviour.js +388 -0
  19. package/dist/cards/cardPile.d.ts +1 -0
  20. package/dist/cards/cardPile.js +12 -3
  21. package/dist/cartridge/runtime.d.ts +15 -0
  22. package/dist/cartridge/runtime.js +556 -0
  23. package/dist/cartridge/spec.d.ts +188 -0
  24. package/dist/cartridge/spec.js +19 -0
  25. package/dist/cartridge/validate.d.ts +2 -0
  26. package/dist/cartridge/validate.js +78 -0
  27. package/dist/combat/abilityKit.d.ts +22 -2
  28. package/dist/combat/abilityKit.js +72 -13
  29. package/dist/combat/castRunner.d.ts +40 -0
  30. package/dist/combat/castRunner.js +56 -0
  31. package/dist/combat/comboPoints.d.ts +14 -0
  32. package/dist/combat/comboPoints.js +52 -0
  33. package/dist/combat/defensiveWindow.d.ts +1 -1
  34. package/dist/combat/defensiveWindow.js +5 -3
  35. package/dist/combat/effects.d.ts +1 -1
  36. package/dist/combat/effects.js +9 -6
  37. package/dist/combat/projectiles.d.ts +64 -6
  38. package/dist/combat/projectiles.js +232 -74
  39. package/dist/combat/resistance.d.ts +16 -4
  40. package/dist/combat/resistance.js +43 -7
  41. package/dist/combat/resourcePool.d.ts +20 -0
  42. package/dist/combat/resourcePool.js +55 -0
  43. package/dist/combat/shotOrigin.d.ts +40 -0
  44. package/dist/combat/shotOrigin.js +57 -0
  45. package/dist/combat/telegraph.d.ts +30 -1
  46. package/dist/combat/telegraph.js +61 -0
  47. package/dist/commands/commandRegistry.d.ts +1 -1
  48. package/dist/commands/commandRegistry.js +2 -1
  49. package/dist/crafting/recipe.js +4 -1
  50. package/dist/data/dataSource.d.ts +26 -0
  51. package/dist/data/dataSource.js +93 -0
  52. package/dist/data/devProxy.d.ts +11 -0
  53. package/dist/data/devProxy.js +38 -0
  54. package/dist/data/fetchJson.d.ts +20 -0
  55. package/dist/data/fetchJson.js +43 -0
  56. package/dist/data/jsonDataSource.d.ts +4 -0
  57. package/dist/data/jsonDataSource.js +6 -0
  58. package/dist/devtools/devtools.d.ts +205 -0
  59. package/dist/devtools/devtools.js +832 -0
  60. package/dist/devtools/transformTunables.d.ts +14 -0
  61. package/dist/devtools/transformTunables.js +38 -0
  62. package/dist/devtools/tunableSchema.d.ts +140 -0
  63. package/dist/devtools/tunableSchema.js +476 -0
  64. package/dist/faction/factions.d.ts +33 -0
  65. package/dist/faction/factions.js +76 -0
  66. package/dist/faction/reputation.d.ts +32 -0
  67. package/dist/faction/reputation.js +82 -0
  68. package/dist/game/chat.d.ts +68 -0
  69. package/dist/game/chat.js +173 -0
  70. package/dist/game/chatFilter.d.ts +15 -0
  71. package/dist/game/chatFilter.js +55 -0
  72. package/dist/game/controlGate.d.ts +4 -0
  73. package/dist/game/controlGate.js +7 -0
  74. package/dist/game/defineGame.d.ts +10 -2
  75. package/dist/game/defineGame.js +2 -1
  76. package/dist/game/events.d.ts +35 -0
  77. package/dist/game/gamePhase.d.ts +20 -0
  78. package/dist/game/gamePhase.js +21 -0
  79. package/dist/game/levelSequence.d.ts +44 -0
  80. package/dist/game/levelSequence.js +78 -0
  81. package/dist/game/lootTable.d.ts +10 -1
  82. package/dist/game/lootTable.js +27 -7
  83. package/dist/game/playableGame.d.ts +286 -6
  84. package/dist/game/playableGame.js +6 -1
  85. package/dist/game/progression.d.ts +1 -0
  86. package/dist/game/progression.js +23 -2
  87. package/dist/game/quest.d.ts +7 -3
  88. package/dist/game/quest.js +17 -8
  89. package/dist/game/race.d.ts +44 -1
  90. package/dist/game/race.js +116 -11
  91. package/dist/game/recordBook.d.ts +29 -0
  92. package/dist/game/recordBook.js +86 -0
  93. package/dist/game/social.d.ts +42 -0
  94. package/dist/game/social.js +122 -0
  95. package/dist/game/spawnPoints.d.ts +19 -0
  96. package/dist/game/spawnPoints.js +23 -0
  97. package/dist/game/talents.d.ts +47 -0
  98. package/dist/game/talents.js +140 -0
  99. package/dist/game/worldItem.d.ts +3 -0
  100. package/dist/game/worldItem.js +12 -6
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/input/actionBindings.d.ts +10 -0
  104. package/dist/input/actionBindings.js +14 -0
  105. package/dist/input/axisInput.d.ts +26 -3
  106. package/dist/input/axisInput.js +44 -4
  107. package/dist/input/bindingOverrides.d.ts +25 -0
  108. package/dist/input/bindingOverrides.js +95 -0
  109. package/dist/input/gestureSurface.d.ts +31 -0
  110. package/dist/input/gestureSurface.js +120 -0
  111. package/dist/input/pointer.d.ts +45 -0
  112. package/dist/input/pointer.js +48 -0
  113. package/dist/input/pointerAxis.d.ts +30 -0
  114. package/dist/input/pointerAxis.js +31 -0
  115. package/dist/input/touchScheme.d.ts +89 -0
  116. package/dist/input/touchScheme.js +153 -0
  117. package/dist/item/affix.d.ts +1 -1
  118. package/dist/item/affix.js +1 -19
  119. package/dist/meta/changelog.d.ts +1 -1
  120. package/dist/meta/changelog.js +59 -1
  121. package/dist/movement/dash.d.ts +3 -0
  122. package/dist/movement/dash.js +19 -5
  123. package/dist/movement/glideModel.d.ts +44 -0
  124. package/dist/movement/glideModel.js +54 -0
  125. package/dist/movement/grappleSwing.d.ts +34 -0
  126. package/dist/movement/grappleSwing.js +56 -0
  127. package/dist/movement/leaderTrail.d.ts +29 -0
  128. package/dist/movement/leaderTrail.js +67 -0
  129. package/dist/movement/movementModel.d.ts +36 -3
  130. package/dist/movement/movementModel.js +81 -5
  131. package/dist/movement/steering.d.ts +27 -0
  132. package/dist/movement/steering.js +40 -0
  133. package/dist/movement/voxelController.d.ts +61 -0
  134. package/dist/movement/voxelController.js +229 -0
  135. package/dist/multiplayer/chatContract.d.ts +42 -0
  136. package/dist/multiplayer/chatContract.js +57 -0
  137. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  138. package/dist/multiplayer/feedWriteGate.js +12 -0
  139. package/dist/multiplayer/identity.d.ts +13 -0
  140. package/dist/multiplayer/identity.js +20 -0
  141. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  142. package/dist/multiplayer/poseSyncGate.js +13 -1
  143. package/dist/multiplayer/presenceModel.d.ts +17 -2
  144. package/dist/multiplayer/presenceModel.js +30 -4
  145. package/dist/multiplayer/voiceContract.d.ts +44 -0
  146. package/dist/multiplayer/voiceContract.js +108 -0
  147. package/dist/nav/corridors.d.ts +30 -0
  148. package/dist/nav/corridors.js +69 -0
  149. package/dist/nav/navConstrain.d.ts +14 -0
  150. package/dist/nav/navConstrain.js +26 -0
  151. package/dist/nav/navFromEnvironment.d.ts +11 -0
  152. package/dist/nav/navFromEnvironment.js +16 -0
  153. package/dist/nav/navGrid.d.ts +10 -0
  154. package/dist/nav/navGrid.js +48 -5
  155. package/dist/nav/pathFollow.d.ts +4 -2
  156. package/dist/nav/pathFollow.js +5 -3
  157. package/dist/nav/railGraph.d.ts +66 -0
  158. package/dist/nav/railGraph.js +129 -0
  159. package/dist/nav/timetable.d.ts +41 -0
  160. package/dist/nav/timetable.js +94 -0
  161. package/dist/physics/ballisticSweep.d.ts +18 -0
  162. package/dist/physics/ballisticSweep.js +49 -0
  163. package/dist/physics/buoyancy.d.ts +6 -0
  164. package/dist/physics/buoyancy.js +14 -0
  165. package/dist/physics/flowTube.d.ts +32 -0
  166. package/dist/physics/flowTube.js +64 -0
  167. package/dist/physics/forceVolume.d.ts +27 -0
  168. package/dist/physics/forceVolume.js +43 -5
  169. package/dist/physics/kinematicVehicle.d.ts +52 -0
  170. package/dist/physics/kinematicVehicle.js +81 -0
  171. package/dist/physics/physicsWorld.d.ts +54 -8
  172. package/dist/physics/physicsWorld.js +400 -79
  173. package/dist/physics/ragdoll.js +1 -1
  174. package/dist/physics/spatialGrid.d.ts +1 -1
  175. package/dist/physics/spatialGrid.js +5 -0
  176. package/dist/physics/vehicleBody.d.ts +6 -0
  177. package/dist/physics/vehicleBody.js +18 -18
  178. package/dist/puzzle/cellGrid.d.ts +26 -0
  179. package/dist/puzzle/cellGrid.js +110 -0
  180. package/dist/puzzle/fallingPiece.d.ts +24 -0
  181. package/dist/puzzle/fallingPiece.js +51 -0
  182. package/dist/random/nameGen.d.ts +18 -0
  183. package/dist/random/nameGen.js +35 -0
  184. package/dist/random/rng.d.ts +2 -0
  185. package/dist/random/rng.js +23 -0
  186. package/dist/random/seedLink.d.ts +5 -0
  187. package/dist/random/seedLink.js +35 -0
  188. package/dist/runtime/adapter.d.ts +35 -0
  189. package/dist/runtime/adapter.js +37 -1
  190. package/dist/runtime/cameraDirector.d.ts +15 -0
  191. package/dist/runtime/cameraDirector.js +31 -0
  192. package/dist/runtime/commandRunner.d.ts +2 -2
  193. package/dist/runtime/commandRunner.js +2 -2
  194. package/dist/runtime/gameContext.d.ts +65 -2
  195. package/dist/runtime/gameContext.js +280 -9
  196. package/dist/runtime/gameRuntime.d.ts +5 -1
  197. package/dist/runtime/gameRuntime.js +13 -12
  198. package/dist/runtime/hostPersistence.d.ts +30 -1
  199. package/dist/runtime/hostPersistence.js +70 -5
  200. package/dist/runtime/inputSnapshot.d.ts +12 -0
  201. package/dist/runtime/inputSnapshot.js +17 -0
  202. package/dist/runtime/motionIntents.d.ts +33 -0
  203. package/dist/runtime/motionIntents.js +43 -0
  204. package/dist/runtime/snapshot.d.ts +1 -0
  205. package/dist/runtime/snapshot.js +1 -1
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/runtime/visibility.d.ts +74 -0
  208. package/dist/runtime/visibility.js +127 -0
  209. package/dist/scene/colliders.d.ts +44 -0
  210. package/dist/scene/colliders.js +77 -0
  211. package/dist/scene/entityStore.d.ts +30 -5
  212. package/dist/scene/entityStore.js +83 -13
  213. package/dist/scene/movementSpeed.d.ts +35 -0
  214. package/dist/scene/movementSpeed.js +20 -0
  215. package/dist/scene/objectQuery.d.ts +29 -0
  216. package/dist/scene/objectQuery.js +140 -0
  217. package/dist/scene/objectStore.d.ts +13 -0
  218. package/dist/scene/objectStore.js +130 -6
  219. package/dist/scene/paintLayer.d.ts +15 -0
  220. package/dist/scene/paintLayer.js +53 -0
  221. package/dist/scene/sceneRaycast.d.ts +80 -0
  222. package/dist/scene/sceneRaycast.js +284 -0
  223. package/dist/scene/spatial.d.ts +17 -1
  224. package/dist/scene/spatial.js +140 -17
  225. package/dist/sensor/concealment.d.ts +21 -0
  226. package/dist/sensor/concealment.js +63 -0
  227. package/dist/sensor/freezeMonitor.d.ts +17 -0
  228. package/dist/sensor/freezeMonitor.js +35 -0
  229. package/dist/sensor/recordingBuffer.d.ts +6 -0
  230. package/dist/sensor/recordingBuffer.js +7 -0
  231. package/dist/sensor/replayLoop.d.ts +39 -0
  232. package/dist/sensor/replayLoop.js +64 -0
  233. package/dist/sensor/visionCone.d.ts +31 -0
  234. package/dist/sensor/visionCone.js +72 -0
  235. package/dist/session/extraction.d.ts +2 -2
  236. package/dist/session/extraction.js +13 -2
  237. package/dist/session/roles.d.ts +6 -0
  238. package/dist/session/roles.js +32 -0
  239. package/dist/session/roundState.d.ts +41 -17
  240. package/dist/session/roundState.js +67 -38
  241. package/dist/settings/settingsModel.d.ts +97 -0
  242. package/dist/settings/settingsModel.js +100 -0
  243. package/dist/stats/statModifiers.d.ts +5 -2
  244. package/dist/stats/statModifiers.js +32 -14
  245. package/dist/store/observableKeyedStore.js +8 -2
  246. package/dist/survival/decayMeter.d.ts +1 -1
  247. package/dist/survival/decayMeter.js +0 -1
  248. package/dist/tactics/fallingGrid.d.ts +62 -0
  249. package/dist/tactics/fallingGrid.js +158 -0
  250. package/dist/tactics/tacticalGrid.d.ts +11 -0
  251. package/dist/tactics/tacticalGrid.js +17 -0
  252. package/dist/time/beatClock.d.ts +18 -0
  253. package/dist/time/beatClock.js +24 -0
  254. package/dist/time/idleProgress.d.ts +29 -0
  255. package/dist/time/idleProgress.js +31 -0
  256. package/dist/time/serverTick.d.ts +7 -1
  257. package/dist/time/serverTick.js +27 -4
  258. package/dist/time/simClock.d.ts +25 -1
  259. package/dist/time/simClock.js +28 -3
  260. package/dist/time/stateSchedule.d.ts +57 -0
  261. package/dist/time/stateSchedule.js +121 -0
  262. package/dist/turn/commit.d.ts +2 -2
  263. package/dist/turn/commit.js +2 -2
  264. package/dist/turn/intent.d.ts +14 -0
  265. package/dist/turn/intent.js +29 -0
  266. package/dist/turn/turnLoop.d.ts +2 -0
  267. package/dist/turn/turnLoop.js +8 -1
  268. package/dist/ui/hudLayout.d.ts +58 -0
  269. package/dist/ui/hudLayout.js +207 -0
  270. package/dist/ui/hudScale.d.ts +44 -0
  271. package/dist/ui/hudScale.js +55 -0
  272. package/dist/visibility/assetStreaming.d.ts +66 -0
  273. package/dist/visibility/assetStreaming.js +201 -0
  274. package/dist/visibility/bounds.d.ts +66 -0
  275. package/dist/visibility/bounds.js +113 -0
  276. package/dist/visibility/camera.d.ts +21 -0
  277. package/dist/visibility/camera.js +3 -0
  278. package/dist/visibility/config.d.ts +21 -0
  279. package/dist/visibility/config.js +1 -0
  280. package/dist/visibility/diagnostics.d.ts +26 -0
  281. package/dist/visibility/diagnostics.js +36 -0
  282. package/dist/visibility/distance.d.ts +11 -0
  283. package/dist/visibility/distance.js +30 -0
  284. package/dist/visibility/frustum.d.ts +47 -0
  285. package/dist/visibility/frustum.js +186 -0
  286. package/dist/visibility/occlusion.d.ts +36 -0
  287. package/dist/visibility/occlusion.js +45 -0
  288. package/dist/visibility/settings.d.ts +75 -0
  289. package/dist/visibility/settings.js +49 -0
  290. package/dist/visibility/simulationCulling.d.ts +34 -0
  291. package/dist/visibility/simulationCulling.js +36 -0
  292. package/dist/visibility/spatialIndex.d.ts +45 -0
  293. package/dist/visibility/spatialIndex.js +198 -0
  294. package/dist/visibility/visibilitySystem.d.ts +90 -0
  295. package/dist/visibility/visibilitySystem.js +257 -0
  296. package/dist/world/buildings.d.ts +6 -0
  297. package/dist/world/buildings.js +152 -0
  298. package/dist/world/carve.d.ts +7 -2
  299. package/dist/world/carve.js +17 -7
  300. package/dist/world/cellStates.d.ts +45 -0
  301. package/dist/world/cellStates.js +103 -0
  302. package/dist/world/environmentSummary.d.ts +61 -0
  303. package/dist/world/environmentSummary.js +138 -0
  304. package/dist/world/features.d.ts +138 -9
  305. package/dist/world/features.js +73 -2
  306. package/dist/world/gridInstances.d.ts +12 -0
  307. package/dist/world/gridInstances.js +160 -0
  308. package/dist/world/lod.d.ts +17 -0
  309. package/dist/world/lod.js +66 -0
  310. package/dist/world/mapLayers.d.ts +60 -0
  311. package/dist/world/mapLayers.js +44 -0
  312. package/dist/world/massing.d.ts +45 -0
  313. package/dist/world/massing.js +581 -0
  314. package/dist/world/minimap.d.ts +13 -1
  315. package/dist/world/minimap.js +21 -2
  316. package/dist/world/placementController.d.ts +22 -1
  317. package/dist/world/placementController.js +56 -0
  318. package/dist/world/terrain.d.ts +62 -1
  319. package/dist/world/terrain.js +253 -9
  320. package/dist/world/voxelField.d.ts +54 -0
  321. package/dist/world/voxelField.js +241 -0
  322. package/dist/world/water.d.ts +7 -0
  323. package/dist/world/water.js +19 -4
  324. package/dist/world/weather.d.ts +3 -18
  325. package/dist/world/weather.js +4 -17
  326. package/dist/world/windZones.d.ts +47 -0
  327. package/dist/world/windZones.js +59 -0
  328. package/llms.txt +4983 -0
  329. package/package.json +14 -3
@@ -0,0 +1,41 @@
1
+ import type { Waypoint } from "./pathFollow.js";
2
+ export interface TimetableStop {
3
+ at: Waypoint;
4
+ /** Seconds the mover holds at this waypoint before departing. */
5
+ dwellSeconds?: number;
6
+ }
7
+ export interface RouteTimetableConfig {
8
+ waypoints: readonly (Waypoint | TimetableStop)[];
9
+ /** World units per second between stops. */
10
+ speed: number;
11
+ /** Wrap from the last waypoint back to the first (a circular line). Default `false` — the mover holds at the terminus. */
12
+ loop?: boolean;
13
+ /** Default dwell applied at every waypoint without its own `dwellSeconds`; default `0`. */
14
+ dwellSeconds?: number;
15
+ /** Shifts the whole timetable so `t = 0` lands mid-route — staggers several movers on one route. */
16
+ offsetSeconds?: number;
17
+ }
18
+ export interface TimetablePose {
19
+ position: Waypoint;
20
+ /** Yaw (radians) of the travel direction, matching engine `rotationY`; holds through dwells. */
21
+ heading: number;
22
+ /** Index of the segment being travelled (or the stop being dwelt at). */
23
+ segment: number;
24
+ /** True while holding at a stop. */
25
+ dwelling: boolean;
26
+ }
27
+ /**
28
+ * A deterministic mover over an authored route: position is a pure function of absolute time, so a
29
+ * forecast preview (`poseAt(t + 30)`) and the live mover can never disagree — the seam `nav/pathFollow`'s
30
+ * dt-incremental state can't offer. Trains, ferries, patrol shuttles, crushers: anything on a timetable.
31
+ */
32
+ export interface RouteTimetable {
33
+ /** Seconds for one full cycle (travel + dwells); non-loop routes hold at the terminus after this. */
34
+ readonly cycleSeconds: number;
35
+ readonly distance: number;
36
+ poseAt(t: number): TimetablePose;
37
+ positionAt(t: number): Waypoint;
38
+ /** Cycle progress in `[0, 1]` — clamps at `1` on a non-looping route's terminus. */
39
+ progressAt(t: number): number;
40
+ }
41
+ export declare function createRouteTimetable(config: RouteTimetableConfig): RouteTimetable;
@@ -0,0 +1,94 @@
1
+ function stopOf(entry) {
2
+ return Array.isArray(entry) ? { at: entry } : entry;
3
+ }
4
+ export function createRouteTimetable(config) {
5
+ if (!(config.speed > 0))
6
+ throw new Error(`timetable speed must be positive, got ${config.speed}`);
7
+ const stops = config.waypoints.map(stopOf);
8
+ if (stops.length < 2)
9
+ throw new Error("timetable needs at least two waypoints");
10
+ const loop = config.loop ?? false;
11
+ const defaultDwell = Math.max(0, config.dwellSeconds ?? 0);
12
+ const offset = config.offsetSeconds ?? 0;
13
+ const legs = [];
14
+ let clock = 0;
15
+ let distance = 0;
16
+ const legCount = loop ? stops.length : stops.length - 1;
17
+ for (let i = 0; i < legCount; i += 1) {
18
+ const from = stops[i];
19
+ const to = stops[(i + 1) % stops.length];
20
+ const dwell = Math.max(0, from.dwellSeconds ?? defaultDwell);
21
+ const dx = to.at[0] - from.at[0];
22
+ const dy = to.at[1] - from.at[1];
23
+ const dz = to.at[2] - from.at[2];
24
+ const length = Math.hypot(dx, dy, dz);
25
+ const heading = length > 1e-9 ? Math.atan2(dx, dz) : 0;
26
+ legs.push({
27
+ from: from.at,
28
+ to: to.at,
29
+ heading,
30
+ startSeconds: clock,
31
+ travelSeconds: length / config.speed,
32
+ dwellSeconds: dwell,
33
+ });
34
+ clock += dwell + length / config.speed;
35
+ distance += length;
36
+ }
37
+ const cycleSeconds = clock;
38
+ const terminus = stops[stops.length - 1].at;
39
+ const lastHeading = legs[legs.length - 1].heading;
40
+ function poseAt(t) {
41
+ let local = t + offset;
42
+ if (loop) {
43
+ local = ((local % cycleSeconds) + cycleSeconds) % cycleSeconds;
44
+ }
45
+ else {
46
+ local = Math.max(0, local);
47
+ if (local >= cycleSeconds) {
48
+ return { position: terminus, heading: lastHeading, segment: legs.length - 1, dwelling: true };
49
+ }
50
+ }
51
+ let leg = legs[legs.length - 1];
52
+ let segment = legs.length - 1;
53
+ for (let i = 0; i < legs.length; i += 1) {
54
+ const candidate = legs[i];
55
+ if (local < candidate.startSeconds + candidate.dwellSeconds + candidate.travelSeconds) {
56
+ leg = candidate;
57
+ segment = i;
58
+ break;
59
+ }
60
+ }
61
+ const intoLeg = local - leg.startSeconds;
62
+ if (intoLeg < leg.dwellSeconds) {
63
+ const previous = legs[(segment - 1 + legs.length) % legs.length];
64
+ const heading = segment === 0 && !loop ? leg.heading : previous.heading;
65
+ return { position: leg.from, heading, segment, dwelling: true };
66
+ }
67
+ const travelled = intoLeg - leg.dwellSeconds;
68
+ const fraction = leg.travelSeconds <= 0 ? 1 : Math.min(1, travelled / leg.travelSeconds);
69
+ return {
70
+ position: [
71
+ leg.from[0] + (leg.to[0] - leg.from[0]) * fraction,
72
+ leg.from[1] + (leg.to[1] - leg.from[1]) * fraction,
73
+ leg.from[2] + (leg.to[2] - leg.from[2]) * fraction,
74
+ ],
75
+ heading: leg.heading,
76
+ segment,
77
+ dwelling: false,
78
+ };
79
+ }
80
+ return {
81
+ cycleSeconds,
82
+ distance,
83
+ poseAt,
84
+ positionAt: (t) => poseAt(t).position,
85
+ progressAt(t) {
86
+ if (cycleSeconds <= 0)
87
+ return 1;
88
+ const local = t + offset;
89
+ if (loop)
90
+ return (((local % cycleSeconds) + cycleSeconds) % cycleSeconds) / cycleSeconds;
91
+ return Math.max(0, Math.min(1, local / cycleSeconds));
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,18 @@
1
+ import type { PhysicsWorld } from "./physicsWorld.js";
2
+ export interface BallisticSweepHit {
3
+ point: [number, number, number];
4
+ time: number;
5
+ }
6
+ export type BallisticSweep = (origin: readonly [number, number, number], velocity: readonly [number, number, number], gravity: number, maxTime: number) => BallisticSweepHit | null;
7
+ export interface BallisticSweepOptions {
8
+ /** Fixed march interval along the arc, in seconds. Default 1/60. */
9
+ step?: number;
10
+ /** Projectile radius; each body AABB is inflated by this before the point test. Default 0. */
11
+ radius?: number;
12
+ }
13
+ /**
14
+ * Marches the closed-form arc (constant gravity, straight lateral) through `world` and reports the
15
+ * first sample inside any live body's AABB — sleeping bodies included — refined by one bisection
16
+ * between the last clear sample and the hit sample. Returns `null` when the whole arc is clear.
17
+ */
18
+ export declare function createBallisticSweep(world: PhysicsWorld, options?: BallisticSweepOptions): BallisticSweep;
@@ -0,0 +1,49 @@
1
+ const DEFAULT_SWEEP_STEP = 1 / 60;
2
+ /**
3
+ * Marches the closed-form arc (constant gravity, straight lateral) through `world` and reports the
4
+ * first sample inside any live body's AABB — sleeping bodies included — refined by one bisection
5
+ * between the last clear sample and the hit sample. Returns `null` when the whole arc is clear.
6
+ */
7
+ export function createBallisticSweep(world, options = {}) {
8
+ const step = options.step ?? DEFAULT_SWEEP_STEP;
9
+ const radius = options.radius ?? 0;
10
+ function overlapsAnyBody(x, y, z) {
11
+ for (let i = 0; i < world.highWater; i += 1) {
12
+ if (!world.isAlive(i))
13
+ continue;
14
+ if (Math.abs(x - world.posX[i]) > world.halfX[i] + radius)
15
+ continue;
16
+ if (Math.abs(y - world.posY[i]) > world.halfY[i] + radius)
17
+ continue;
18
+ if (Math.abs(z - world.posZ[i]) > world.halfZ[i] + radius)
19
+ continue;
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+ return (origin, velocity, gravity, maxTime) => {
25
+ const pointAt = (t) => [
26
+ origin[0] + velocity[0] * t,
27
+ origin[1] + velocity[1] * t - 0.5 * gravity * t * t,
28
+ origin[2] + velocity[2] * t,
29
+ ];
30
+ let lastClear = 0;
31
+ let t = 0;
32
+ for (;;) {
33
+ const sample = pointAt(t);
34
+ if (overlapsAnyBody(sample[0], sample[1], sample[2])) {
35
+ if (t === 0)
36
+ return { point: sample, time: 0 };
37
+ const mid = (lastClear + t) / 2;
38
+ const midSample = pointAt(mid);
39
+ return overlapsAnyBody(midSample[0], midSample[1], midSample[2])
40
+ ? { point: midSample, time: mid }
41
+ : { point: sample, time: t };
42
+ }
43
+ if (t >= maxTime)
44
+ return null;
45
+ lastClear = t;
46
+ t = Math.min(t + step, maxTime);
47
+ }
48
+ };
49
+ }
@@ -15,6 +15,10 @@ export interface BuoyantBodyConfig {
15
15
  topSpeed?: number;
16
16
  turnRate?: number;
17
17
  turnSpeedRef?: number;
18
+ /** Water-current acceleration (units/s²) sampled at the hull each tick and applied while submerged — feed a `windZones`/`flowTube` sample or a fixed vector. */
19
+ current?: (x: number, z: number, time: number) => readonly [number, number];
20
+ /** How much more current a broadside hull catches than a bow-on one (`1` = uniform, default; `2` = side-on drifts twice as hard). */
21
+ currentBroadside?: number;
18
22
  }
19
23
  /**
20
24
  * Floats a {@link PhysicsWorld} body on a CPU {@link WaterSurface}: each hull sample point pushes the
@@ -38,6 +42,8 @@ export declare class BuoyantBody {
38
42
  private readonly topSpeed;
39
43
  private readonly turnRate;
40
44
  private readonly turnSpeedRef;
45
+ private readonly current;
46
+ private readonly currentBroadside;
41
47
  private submergedFlag;
42
48
  constructor(world: PhysicsWorld, config: BuoyantBodyConfig);
43
49
  get position(): [number, number, number];
@@ -26,6 +26,8 @@ export class BuoyantBody {
26
26
  topSpeed;
27
27
  turnRate;
28
28
  turnSpeedRef;
29
+ current;
30
+ currentBroadside;
29
31
  submergedFlag = false;
30
32
  constructor(world, config) {
31
33
  this.world = world;
@@ -42,6 +44,8 @@ export class BuoyantBody {
42
44
  this.topSpeed = config.topSpeed ?? 14;
43
45
  this.turnRate = config.turnRate ?? 1.2;
44
46
  this.turnSpeedRef = config.turnSpeedRef ?? 4;
47
+ this.current = config.current ?? null;
48
+ this.currentBroadside = config.currentBroadside ?? 1;
45
49
  }
46
50
  get position() {
47
51
  return [this.world.posX[this.body], this.world.posY[this.body], this.world.posZ[this.body]];
@@ -81,6 +85,16 @@ export class BuoyantBody {
81
85
  const drag = Math.max(0, 1 - this.linearDrag * dt);
82
86
  w.velX[b] *= drag;
83
87
  w.velZ[b] *= drag;
88
+ if (this.current !== null) {
89
+ const [cx, cz] = this.current(w.posX[b], w.posZ[b], time);
90
+ const magnitude = Math.hypot(cx, cz);
91
+ if (magnitude > 0) {
92
+ const alignment = Math.abs((cx * sin + cz * cos) / magnitude);
93
+ const scale = this.currentBroadside + (1 - this.currentBroadside) * alignment;
94
+ w.velX[b] += cx * scale * dt;
95
+ w.velZ[b] += cz * scale * dt;
96
+ }
97
+ }
84
98
  this.submergedFlag = true;
85
99
  w.wake(b);
86
100
  }
@@ -0,0 +1,32 @@
1
+ export type FlowVec3 = readonly [number, number, number];
2
+ export interface FlowTubeConfig {
3
+ from: FlowVec3;
4
+ to: FlowVec3;
5
+ /** Tube radius; flow strength fades from the axis to zero here. */
6
+ radius: number;
7
+ /** Axial flow speed on the core at full spool. */
8
+ strength: number;
9
+ /** Radial falloff exponent — `1` linear, `2` (default) keeps a fat core with soft edges. */
10
+ falloff?: number;
11
+ /** Extra reach past each end where the axial envelope fades to zero; default `0` (hard caps). */
12
+ capFalloff?: number;
13
+ }
14
+ /**
15
+ * An axial corridor of directional flow with radial core falloff and a spool scalar — fan tunnels,
16
+ * updraft shafts, river narrows, thruster wash. Pure math: sample `velocityAt(point, spool)` and add
17
+ * it to whatever integrator moves the body (walk controller drift, `BuoyantBody`, a custom sim).
18
+ */
19
+ export interface FlowTube {
20
+ readonly axis: FlowVec3;
21
+ readonly length: number;
22
+ /** Flow velocity at a world point, scaled by `spool` in `[0, 1]` (default `1`); zero outside the tube. */
23
+ velocityAt(point: FlowVec3, spool?: number): FlowVec3;
24
+ /** The radial × axial envelope in `[0, 1]` at a world point — feed particle density, audio gain, rumble. */
25
+ intensityAt(point: FlowVec3): number;
26
+ }
27
+ export declare function createFlowTube(config: FlowTubeConfig): FlowTube;
28
+ /** Sum several tubes' flow at one point — each entry carries its own spool. */
29
+ export declare function combineFlowVelocity(tubes: readonly (FlowTube | {
30
+ tube: FlowTube;
31
+ spool: number;
32
+ })[], point: FlowVec3): FlowVec3;
@@ -0,0 +1,64 @@
1
+ export function createFlowTube(config) {
2
+ if (!(config.radius > 0))
3
+ throw new Error(`flow tube radius must be positive, got ${config.radius}`);
4
+ const [fx, fy, fz] = config.from;
5
+ const dx = config.to[0] - fx;
6
+ const dy = config.to[1] - fy;
7
+ const dz = config.to[2] - fz;
8
+ const length = Math.hypot(dx, dy, dz);
9
+ if (length <= 1e-9)
10
+ throw new Error("flow tube needs distinct from/to points");
11
+ const axis = [dx / length, dy / length, dz / length];
12
+ const falloff = config.falloff ?? 2;
13
+ const capFalloff = Math.max(0, config.capFalloff ?? 0);
14
+ function envelopeAt(point) {
15
+ const px = point[0] - fx;
16
+ const py = point[1] - fy;
17
+ const pz = point[2] - fz;
18
+ const along = px * axis[0] + py * axis[1] + pz * axis[2];
19
+ if (along < -capFalloff || along > length + capFalloff)
20
+ return 0;
21
+ const axial = capFalloff <= 0
22
+ ? along >= 0 && along <= length
23
+ ? 1
24
+ : 0
25
+ : along < 0
26
+ ? 1 + along / capFalloff
27
+ : along > length
28
+ ? 1 - (along - length) / capFalloff
29
+ : 1;
30
+ if (axial <= 0)
31
+ return 0;
32
+ const rx = px - axis[0] * along;
33
+ const ry = py - axis[1] * along;
34
+ const rz = pz - axis[2] * along;
35
+ const radial = Math.hypot(rx, ry, rz);
36
+ if (radial >= config.radius)
37
+ return 0;
38
+ return axial * Math.pow(1 - radial / config.radius, falloff);
39
+ }
40
+ return {
41
+ axis,
42
+ length,
43
+ velocityAt(point, spool = 1) {
44
+ const scale = envelopeAt(point) * config.strength * Math.max(0, Math.min(1, spool));
45
+ return scale <= 0 ? [0, 0, 0] : [axis[0] * scale, axis[1] * scale, axis[2] * scale];
46
+ },
47
+ intensityAt: envelopeAt,
48
+ };
49
+ }
50
+ /** Sum several tubes' flow at one point — each entry carries its own spool. */
51
+ export function combineFlowVelocity(tubes, point) {
52
+ let x = 0;
53
+ let y = 0;
54
+ let z = 0;
55
+ for (const entry of tubes) {
56
+ const tube = "velocityAt" in entry ? entry : entry.tube;
57
+ const spool = "velocityAt" in entry ? 1 : entry.spool;
58
+ const [vx, vy, vz] = tube.velocityAt(point, spool);
59
+ x += vx;
60
+ y += vy;
61
+ z += vz;
62
+ }
63
+ return [x, y, z];
64
+ }
@@ -27,6 +27,33 @@ export declare class ForceVolume {
27
27
  constructor(config: ForceVolumeConfig);
28
28
  apply(world: PhysicsWorld, dt: number): void;
29
29
  }
30
+ /** One tick's force math for a body outside `PhysicsWorld` — apply the returned velocity in any custom integrator. */
31
+ export declare function applyVolumeForce(velocity: readonly [number, number, number], force: readonly [number, number, number], mode: ForceMode, dt: number): readonly [number, number, number];
32
+ export interface VolumeTriggerConfig {
33
+ bounds: PhysicsBounds;
34
+ }
35
+ export interface VolumeTriggerStep<TId> {
36
+ /** Ids inside this tick that were outside last tick — the boost-pad edge. */
37
+ entered: readonly TId[];
38
+ /** Every id inside this tick. */
39
+ inside: readonly TId[];
40
+ /** Ids inside last tick that left. */
41
+ exited: readonly TId[];
42
+ }
43
+ /**
44
+ * The enter-once membership tracking from `ForceVolume`, freed from `PhysicsWorld`'s body indices
45
+ * (#286.8): feed any integrator's `{ id, position }` list each tick and act on the edges — apply
46
+ * `applyVolumeForce` on `entered` for a boost pad, on `inside` for a fan.
47
+ */
48
+ export interface VolumeTrigger<TId> {
49
+ readonly bounds: PhysicsBounds;
50
+ step(bodies: Iterable<{
51
+ id: TId;
52
+ position: readonly [number, number, number];
53
+ }>): VolumeTriggerStep<TId>;
54
+ reset(): void;
55
+ }
56
+ export declare function createVolumeTrigger<TId = string>(config: VolumeTriggerConfig): VolumeTrigger<TId>;
30
57
  export interface PlatformCarryConfig {
31
58
  /** Vertical gap between a rider's base and the platform top counted as "standing on". Default 0.12. */
32
59
  contactTolerance?: number;
@@ -31,8 +31,8 @@ export class ForceVolume {
31
31
  apply(world, dt) {
32
32
  const next = this.nextMembers;
33
33
  next.clear();
34
- const count = world.count;
35
- for (let i = 0; i < count; i += 1) {
34
+ const high = world.highWater;
35
+ for (let i = 0; i < high; i += 1) {
36
36
  if (world.invMass[i] === 0)
37
37
  continue;
38
38
  if (!inside(this.bounds, world.posX[i], world.posY[i], world.posZ[i]))
@@ -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
@@ -99,9 +137,9 @@ export class PlatformCarry {
99
137
  return;
100
138
  const hx = w.halfX[p];
101
139
  const hz = w.halfZ[p];
102
- const count = w.count;
103
- for (let i = 0; i < count; i += 1) {
104
- if (i === p)
140
+ const high = w.highWater;
141
+ for (let i = 0; i < high; i += 1) {
142
+ if (i === p || !w.isAlive(i))
105
143
  continue;
106
144
  const base = w.posY[i] - w.halfY[i];
107
145
  if (base < topY - this.tolerance || base > topY + this.tolerance)
@@ -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
+ }