@jgengine/core 0.6.0 → 0.8.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 (330) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +2 -0
  3. package/dist/ai/crowd.d.ts +36 -0
  4. package/dist/ai/crowd.js +213 -0
  5. package/dist/ai/jobBoard.d.ts +49 -0
  6. package/dist/ai/jobBoard.js +155 -0
  7. package/dist/ai/spawnDirector.d.ts +61 -0
  8. package/dist/ai/spawnDirector.js +162 -0
  9. package/dist/ai/threat.d.ts +25 -0
  10. package/dist/ai/threat.js +67 -0
  11. package/dist/anim/easing.d.ts +14 -0
  12. package/dist/anim/easing.js +55 -0
  13. package/dist/audio/audioFalloff.d.ts +38 -0
  14. package/dist/audio/audioFalloff.js +32 -0
  15. package/dist/behaviour/behaviour.d.ts +136 -0
  16. package/dist/behaviour/behaviour.js +388 -0
  17. package/dist/board/laneBoard.d.ts +63 -0
  18. package/dist/board/laneBoard.js +114 -0
  19. package/dist/board/timelineBoard.d.ts +36 -0
  20. package/dist/board/timelineBoard.js +70 -0
  21. package/dist/cards/cardPile.d.ts +64 -0
  22. package/dist/cards/cardPile.js +175 -0
  23. package/dist/cards/modifierPipeline.d.ts +33 -0
  24. package/dist/cards/modifierPipeline.js +48 -0
  25. package/dist/combat/abilityKit.d.ts +47 -0
  26. package/dist/combat/abilityKit.js +139 -0
  27. package/dist/combat/animationState.d.ts +47 -0
  28. package/dist/combat/animationState.js +101 -0
  29. package/dist/combat/attackTags.d.ts +14 -0
  30. package/dist/combat/attackTags.js +26 -0
  31. package/dist/combat/breakMeters.d.ts +32 -0
  32. package/dist/combat/breakMeters.js +51 -0
  33. package/dist/combat/comboString.d.ts +35 -0
  34. package/dist/combat/comboString.js +59 -0
  35. package/dist/combat/death.d.ts +8 -0
  36. package/dist/combat/death.js +5 -2
  37. package/dist/combat/defensiveWindow.d.ts +34 -0
  38. package/dist/combat/defensiveWindow.js +66 -0
  39. package/dist/combat/downed.d.ts +35 -0
  40. package/dist/combat/downed.js +104 -0
  41. package/dist/combat/effects.d.ts +12 -1
  42. package/dist/combat/effects.js +31 -19
  43. package/dist/combat/hitReaction.d.ts +23 -0
  44. package/dist/combat/hitReaction.js +20 -0
  45. package/dist/combat/projectiles.d.ts +42 -5
  46. package/dist/combat/projectiles.js +98 -12
  47. package/dist/combat/resistance.d.ts +13 -0
  48. package/dist/combat/resistance.js +46 -0
  49. package/dist/combat/telegraph.d.ts +31 -0
  50. package/dist/combat/telegraph.js +51 -0
  51. package/dist/commands/commandRegistry.d.ts +1 -1
  52. package/dist/commands/commandRegistry.js +2 -1
  53. package/dist/crafting/crop.d.ts +59 -0
  54. package/dist/crafting/crop.js +181 -0
  55. package/dist/crafting/production.d.ts +64 -0
  56. package/dist/crafting/production.js +130 -0
  57. package/dist/crafting/recipe.d.ts +62 -0
  58. package/dist/crafting/recipe.js +101 -0
  59. package/dist/data/dataSource.d.ts +26 -0
  60. package/dist/data/dataSource.js +93 -0
  61. package/dist/data/devProxy.d.ts +11 -0
  62. package/dist/data/devProxy.js +38 -0
  63. package/dist/data/fetchJson.d.ts +20 -0
  64. package/dist/data/fetchJson.js +43 -0
  65. package/dist/data/jsonDataSource.d.ts +4 -0
  66. package/dist/data/jsonDataSource.js +6 -0
  67. package/dist/devtools/devtools.d.ts +157 -0
  68. package/dist/devtools/devtools.js +484 -0
  69. package/dist/devtools/transformTunables.d.ts +14 -0
  70. package/dist/devtools/transformTunables.js +38 -0
  71. package/dist/economy/sharedWallet.d.ts +28 -0
  72. package/dist/economy/sharedWallet.js +62 -0
  73. package/dist/economy/techTree.d.ts +40 -0
  74. package/dist/economy/techTree.js +66 -0
  75. package/dist/game/chat.d.ts +66 -0
  76. package/dist/game/chat.js +165 -0
  77. package/dist/game/cosmetics.d.ts +25 -0
  78. package/dist/game/cosmetics.js +51 -0
  79. package/dist/game/defineGame.d.ts +10 -2
  80. package/dist/game/defineGame.js +2 -1
  81. package/dist/game/events.d.ts +87 -0
  82. package/dist/game/levelSequence.d.ts +44 -0
  83. package/dist/game/levelSequence.js +78 -0
  84. package/dist/game/lootFilter.d.ts +33 -0
  85. package/dist/game/lootFilter.js +48 -0
  86. package/dist/game/ping.d.ts +67 -0
  87. package/dist/game/ping.js +86 -0
  88. package/dist/game/playableGame.d.ts +516 -8
  89. package/dist/game/playableGame.js +6 -1
  90. package/dist/game/progression.d.ts +1 -0
  91. package/dist/game/progression.js +23 -2
  92. package/dist/game/race.d.ts +100 -0
  93. package/dist/game/race.js +236 -0
  94. package/dist/game/runDraft.d.ts +34 -0
  95. package/dist/game/runDraft.js +97 -0
  96. package/dist/game/social.d.ts +69 -0
  97. package/dist/game/social.js +138 -0
  98. package/dist/game/spawnPoints.d.ts +19 -0
  99. package/dist/game/spawnPoints.js +23 -0
  100. package/dist/game/worldItem.d.ts +95 -0
  101. package/dist/game/worldItem.js +138 -0
  102. package/dist/input/actionBindings.d.ts +10 -0
  103. package/dist/input/actionBindings.js +14 -0
  104. package/dist/input/axisInput.d.ts +43 -0
  105. package/dist/input/axisInput.js +70 -0
  106. package/dist/input/gestureSurface.d.ts +31 -0
  107. package/dist/input/gestureSurface.js +120 -0
  108. package/dist/input/pointer.d.ts +69 -0
  109. package/dist/input/pointer.js +65 -0
  110. package/dist/input/touchScheme.d.ts +83 -0
  111. package/dist/input/touchScheme.js +115 -0
  112. package/dist/interaction/contextMenu.d.ts +30 -0
  113. package/dist/interaction/contextMenu.js +22 -0
  114. package/dist/interaction/qte.d.ts +20 -0
  115. package/dist/interaction/qte.js +24 -0
  116. package/dist/interaction/skillCheck.d.ts +20 -0
  117. package/dist/interaction/skillCheck.js +26 -0
  118. package/dist/inventory/shapedGrid.d.ts +56 -0
  119. package/dist/inventory/shapedGrid.js +176 -0
  120. package/dist/inventory/storageTier.d.ts +52 -0
  121. package/dist/inventory/storageTier.js +77 -0
  122. package/dist/item/affix.d.ts +54 -0
  123. package/dist/item/affix.js +115 -0
  124. package/dist/item/durability.d.ts +51 -0
  125. package/dist/item/durability.js +78 -0
  126. package/dist/item/modularItem.d.ts +46 -0
  127. package/dist/item/modularItem.js +75 -0
  128. package/dist/meta/changelog.d.ts +1 -1
  129. package/dist/meta/changelog.js +71 -1
  130. package/dist/movement/dash.d.ts +37 -0
  131. package/dist/movement/dash.js +65 -0
  132. package/dist/movement/movementModel.d.ts +36 -3
  133. package/dist/movement/movementModel.js +81 -5
  134. package/dist/movement/voxelController.d.ts +61 -0
  135. package/dist/movement/voxelController.js +229 -0
  136. package/dist/multiplayer/chatContract.d.ts +42 -0
  137. package/dist/multiplayer/chatContract.js +57 -0
  138. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  139. package/dist/multiplayer/combatSnapshot.js +89 -0
  140. package/dist/multiplayer/identity.d.ts +13 -0
  141. package/dist/multiplayer/identity.js +20 -0
  142. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  143. package/dist/multiplayer/lagCompensation.js +154 -0
  144. package/dist/multiplayer/matchmaking.d.ts +33 -0
  145. package/dist/multiplayer/matchmaking.js +60 -0
  146. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  147. package/dist/multiplayer/poseSyncGate.js +13 -1
  148. package/dist/multiplayer/presenceModel.d.ts +3 -0
  149. package/dist/multiplayer/presenceModel.js +15 -1
  150. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  151. package/dist/multiplayer/simultaneousCommit.js +53 -0
  152. package/dist/multiplayer/voiceContract.d.ts +44 -0
  153. package/dist/multiplayer/voiceContract.js +108 -0
  154. package/dist/nav/navConstrain.d.ts +14 -0
  155. package/dist/nav/navConstrain.js +26 -0
  156. package/dist/nav/navFromEnvironment.d.ts +11 -0
  157. package/dist/nav/navFromEnvironment.js +16 -0
  158. package/dist/nav/navGrid.d.ts +56 -0
  159. package/dist/nav/navGrid.js +265 -0
  160. package/dist/nav/pathFollow.d.ts +31 -0
  161. package/dist/nav/pathFollow.js +69 -0
  162. package/dist/physics/ballisticSweep.d.ts +18 -0
  163. package/dist/physics/ballisticSweep.js +49 -0
  164. package/dist/physics/buoyancy.d.ts +49 -0
  165. package/dist/physics/buoyancy.js +112 -0
  166. package/dist/physics/carryable.d.ts +46 -0
  167. package/dist/physics/carryable.js +110 -0
  168. package/dist/physics/damageZones.d.ts +56 -0
  169. package/dist/physics/damageZones.js +107 -0
  170. package/dist/physics/forceVolume.d.ts +48 -0
  171. package/dist/physics/forceVolume.js +119 -0
  172. package/dist/physics/physicsWorld.d.ts +124 -2
  173. package/dist/physics/physicsWorld.js +493 -41
  174. package/dist/physics/ragdoll.d.ts +49 -0
  175. package/dist/physics/ragdoll.js +103 -0
  176. package/dist/physics/spatialGrid.d.ts +41 -0
  177. package/dist/physics/spatialGrid.js +143 -0
  178. package/dist/physics/structure.d.ts +83 -0
  179. package/dist/physics/structure.js +0 -0
  180. package/dist/physics/traversal.d.ts +75 -0
  181. package/dist/physics/traversal.js +138 -0
  182. package/dist/physics/vehicleBody.d.ts +99 -0
  183. package/dist/physics/vehicleBody.js +233 -0
  184. package/dist/puzzle/cellGrid.d.ts +26 -0
  185. package/dist/puzzle/cellGrid.js +110 -0
  186. package/dist/puzzle/fallingPiece.d.ts +24 -0
  187. package/dist/puzzle/fallingPiece.js +51 -0
  188. package/dist/random/nameGen.d.ts +18 -0
  189. package/dist/random/nameGen.js +35 -0
  190. package/dist/random/rng.d.ts +2 -0
  191. package/dist/random/rng.js +23 -0
  192. package/dist/runtime/adapter.d.ts +35 -0
  193. package/dist/runtime/adapter.js +37 -1
  194. package/dist/runtime/cameraDirector.d.ts +10 -0
  195. package/dist/runtime/cameraDirector.js +25 -0
  196. package/dist/runtime/gameContext.d.ts +107 -3
  197. package/dist/runtime/gameContext.js +253 -7
  198. package/dist/runtime/hostPersistence.d.ts +39 -0
  199. package/dist/runtime/hostPersistence.js +54 -0
  200. package/dist/runtime/inputSnapshot.d.ts +7 -0
  201. package/dist/runtime/inputSnapshot.js +12 -0
  202. package/dist/runtime/motionIntents.d.ts +25 -0
  203. package/dist/runtime/motionIntents.js +25 -0
  204. package/dist/runtime/persistenceScope.d.ts +32 -0
  205. package/dist/runtime/persistenceScope.js +64 -0
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/scene/autoTarget.d.ts +15 -0
  208. package/dist/scene/autoTarget.js +54 -0
  209. package/dist/scene/behaviors.d.ts +13 -1
  210. package/dist/scene/behaviors.js +3 -0
  211. package/dist/scene/captureCheck.d.ts +7 -0
  212. package/dist/scene/captureCheck.js +11 -0
  213. package/dist/scene/entityStore.d.ts +9 -1
  214. package/dist/scene/entityStore.js +25 -2
  215. package/dist/scene/form.d.ts +40 -0
  216. package/dist/scene/form.js +56 -0
  217. package/dist/scene/mount.d.ts +62 -0
  218. package/dist/scene/mount.js +107 -0
  219. package/dist/scene/movementSpeed.d.ts +35 -0
  220. package/dist/scene/movementSpeed.js +20 -0
  221. package/dist/scene/objectQuery.d.ts +18 -0
  222. package/dist/scene/objectQuery.js +108 -0
  223. package/dist/scene/objectStore.d.ts +11 -0
  224. package/dist/scene/objectStore.js +109 -4
  225. package/dist/scene/paintLayer.d.ts +15 -0
  226. package/dist/scene/paintLayer.js +53 -0
  227. package/dist/scene/possession.d.ts +30 -0
  228. package/dist/scene/possession.js +51 -0
  229. package/dist/scene/roster.d.ts +25 -0
  230. package/dist/scene/roster.js +57 -0
  231. package/dist/scene/selection.d.ts +30 -0
  232. package/dist/scene/selection.js +38 -0
  233. package/dist/scene/spatial.d.ts +21 -0
  234. package/dist/scene/spatial.js +63 -1
  235. package/dist/scene/stationClaim.d.ts +43 -0
  236. package/dist/scene/stationClaim.js +109 -0
  237. package/dist/sensor/concealment.d.ts +21 -0
  238. package/dist/sensor/concealment.js +63 -0
  239. package/dist/sensor/freezeMonitor.d.ts +17 -0
  240. package/dist/sensor/freezeMonitor.js +35 -0
  241. package/dist/sensor/frustumSensor.d.ts +60 -0
  242. package/dist/sensor/frustumSensor.js +104 -0
  243. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  244. package/dist/sensor/hiddenStateProbe.js +40 -0
  245. package/dist/sensor/recordingBuffer.d.ts +29 -0
  246. package/dist/sensor/recordingBuffer.js +61 -0
  247. package/dist/sensor/revealQuery.d.ts +22 -0
  248. package/dist/sensor/revealQuery.js +36 -0
  249. package/dist/session/contestedChannel.d.ts +36 -0
  250. package/dist/session/contestedChannel.js +100 -0
  251. package/dist/session/extraction.d.ts +54 -0
  252. package/dist/session/extraction.js +108 -0
  253. package/dist/session/ring.d.ts +36 -0
  254. package/dist/session/ring.js +88 -0
  255. package/dist/session/roles.d.ts +6 -0
  256. package/dist/session/roles.js +32 -0
  257. package/dist/session/roundState.d.ts +79 -0
  258. package/dist/session/roundState.js +158 -0
  259. package/dist/stats/accumulatorMeter.d.ts +31 -0
  260. package/dist/stats/accumulatorMeter.js +81 -0
  261. package/dist/stats/eventMeter.d.ts +34 -0
  262. package/dist/stats/eventMeter.js +76 -0
  263. package/dist/stats/rollCheck.d.ts +15 -0
  264. package/dist/stats/rollCheck.js +15 -0
  265. package/dist/survival/decayMeter.d.ts +59 -0
  266. package/dist/survival/decayMeter.js +95 -0
  267. package/dist/survival/moodle.d.ts +48 -0
  268. package/dist/survival/moodle.js +91 -0
  269. package/dist/survival/regionHealth.d.ts +72 -0
  270. package/dist/survival/regionHealth.js +129 -0
  271. package/dist/tactics/fallingGrid.d.ts +62 -0
  272. package/dist/tactics/fallingGrid.js +158 -0
  273. package/dist/tactics/predictiveQuery.d.ts +34 -0
  274. package/dist/tactics/predictiveQuery.js +44 -0
  275. package/dist/tactics/snapshot.d.ts +18 -0
  276. package/dist/tactics/snapshot.js +65 -0
  277. package/dist/tactics/surface.d.ts +52 -0
  278. package/dist/tactics/surface.js +135 -0
  279. package/dist/tactics/tacticalGrid.d.ts +68 -0
  280. package/dist/tactics/tacticalGrid.js +261 -0
  281. package/dist/time/beatClock.d.ts +40 -0
  282. package/dist/time/beatClock.js +73 -0
  283. package/dist/time/simClock.d.ts +25 -1
  284. package/dist/time/simClock.js +28 -3
  285. package/dist/turn/commit.d.ts +27 -0
  286. package/dist/turn/commit.js +72 -0
  287. package/dist/turn/intent.d.ts +14 -0
  288. package/dist/turn/intent.js +29 -0
  289. package/dist/turn/turnLoop.d.ts +60 -0
  290. package/dist/turn/turnLoop.js +223 -0
  291. package/dist/world/buildPermissions.d.ts +47 -0
  292. package/dist/world/buildPermissions.js +134 -0
  293. package/dist/world/carve.d.ts +105 -0
  294. package/dist/world/carve.js +175 -0
  295. package/dist/world/connectors.d.ts +40 -0
  296. package/dist/world/connectors.js +67 -0
  297. package/dist/world/envField.d.ts +71 -0
  298. package/dist/world/envField.js +96 -0
  299. package/dist/world/environmentSummary.d.ts +53 -0
  300. package/dist/world/environmentSummary.js +129 -0
  301. package/dist/world/features.d.ts +87 -6
  302. package/dist/world/features.js +52 -2
  303. package/dist/world/fog.d.ts +40 -0
  304. package/dist/world/fog.js +137 -0
  305. package/dist/world/gridInstances.d.ts +13 -0
  306. package/dist/world/gridInstances.js +22 -0
  307. package/dist/world/markers.d.ts +52 -0
  308. package/dist/world/markers.js +96 -0
  309. package/dist/world/minimap.d.ts +42 -0
  310. package/dist/world/minimap.js +72 -0
  311. package/dist/world/placedStructureStore.d.ts +39 -0
  312. package/dist/world/placedStructureStore.js +95 -0
  313. package/dist/world/placementController.d.ts +69 -0
  314. package/dist/world/placementController.js +153 -0
  315. package/dist/world/realm.d.ts +61 -0
  316. package/dist/world/realm.js +66 -0
  317. package/dist/world/support.d.ts +27 -0
  318. package/dist/world/support.js +75 -0
  319. package/dist/world/terraform.d.ts +54 -0
  320. package/dist/world/terraform.js +186 -0
  321. package/dist/world/terrain.d.ts +23 -1
  322. package/dist/world/terrain.js +64 -1
  323. package/dist/world/voxelField.d.ts +54 -0
  324. package/dist/world/voxelField.js +241 -0
  325. package/dist/world/walls.d.ts +61 -0
  326. package/dist/world/walls.js +221 -0
  327. package/dist/world/weather.d.ts +92 -0
  328. package/dist/world/weather.js +169 -0
  329. package/llms.txt +4102 -0
  330. package/package.json +3 -2
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Movement multiplier (1 = unhindered, →0 = crushed) for a body of `mass` carried by `owners`.
3
+ * Pure — the HUD/movement kit reads it to slow a laden hauler (Lethal Company) and to gate items
4
+ * that need 2+ people (R.E.P.O.).
5
+ */
6
+ export function carrySpeedMultiplier(mass, carryCapacity, owners) {
7
+ if (owners <= 0)
8
+ return 0;
9
+ if (mass <= 0)
10
+ return 1;
11
+ const scale = (carryCapacity * owners) / mass;
12
+ return scale < 0 ? 0 : scale > 1 ? 1 : scale;
13
+ }
14
+ /**
15
+ * A grabbed physics object following a moving hold point through a spring constraint (the pick —
16
+ * a raycast — is the caller's/shell's job; core owns the constraint). Supports shared multi-owner
17
+ * carry (the follow point is the average of owners' hold points), an encumbrance read, and
18
+ * drop/throw. Reuses `PhysicsWorld.springJoint` to a world anchor moved each frame.
19
+ */
20
+ export class Carryable {
21
+ body;
22
+ world;
23
+ stiffness;
24
+ damping;
25
+ maxForce;
26
+ capacity;
27
+ holds = new Map();
28
+ joint = -1;
29
+ constructor(world, body, config = {}) {
30
+ this.world = world;
31
+ this.body = body;
32
+ this.stiffness = config.followStiffness ?? 60;
33
+ this.damping = config.followDamping ?? 12;
34
+ this.maxForce = config.maxForce ?? 40;
35
+ this.capacity = config.carryCapacity ?? 8;
36
+ }
37
+ get held() {
38
+ return this.holds.size > 0;
39
+ }
40
+ get owners() {
41
+ return this.holds.size;
42
+ }
43
+ mass() {
44
+ const im = this.world.invMass[this.body];
45
+ return im > 0 ? 1 / im : Number.POSITIVE_INFINITY;
46
+ }
47
+ speedMultiplier() {
48
+ return carrySpeedMultiplier(this.mass(), this.capacity, this.holds.size);
49
+ }
50
+ /** Add an owner grabbing at a hold point; the first owner creates the follow constraint. */
51
+ grab(ownerId, x, y, z) {
52
+ this.holds.set(ownerId, [x, y, z]);
53
+ if (this.joint < 0) {
54
+ this.joint = this.world.springJoint({
55
+ bodyA: this.body,
56
+ restLength: 0,
57
+ stiffness: this.stiffness,
58
+ damping: this.damping,
59
+ maxImpulse: this.maxForce,
60
+ anchorB: [x, y, z],
61
+ });
62
+ }
63
+ this.world.wake(this.body);
64
+ }
65
+ setHoldPoint(ownerId, x, y, z) {
66
+ const h = this.holds.get(ownerId);
67
+ if (h === undefined)
68
+ return;
69
+ h[0] = x;
70
+ h[1] = y;
71
+ h[2] = z;
72
+ }
73
+ release(ownerId) {
74
+ this.holds.delete(ownerId);
75
+ if (this.holds.size === 0 && this.joint >= 0) {
76
+ this.world.removeJoint(this.joint);
77
+ this.joint = -1;
78
+ }
79
+ }
80
+ /** Recompute the follow point (average of live hold points) and drive the constraint. */
81
+ update() {
82
+ if (this.joint < 0 || this.holds.size === 0)
83
+ return;
84
+ let tx = 0;
85
+ let ty = 0;
86
+ let tz = 0;
87
+ for (const h of this.holds.values()) {
88
+ tx += h[0];
89
+ ty += h[1];
90
+ tz += h[2];
91
+ }
92
+ const inv = 1 / this.holds.size;
93
+ this.world.setJointAnchor(this.joint, tx * inv, ty * inv, tz * inv);
94
+ this.world.wake(this.body);
95
+ }
96
+ drop() {
97
+ this.holds.clear();
98
+ if (this.joint >= 0) {
99
+ this.world.removeJoint(this.joint);
100
+ this.joint = -1;
101
+ }
102
+ }
103
+ throw(vx, vy, vz) {
104
+ this.drop();
105
+ this.world.velX[this.body] = vx;
106
+ this.world.velY[this.body] = vy;
107
+ this.world.velZ[this.body] = vz;
108
+ this.world.wake(this.body);
109
+ }
110
+ }
@@ -0,0 +1,56 @@
1
+ import type { CollisionEvent } from "./physicsWorld.js";
2
+ export interface DamageZoneDef {
3
+ id: string;
4
+ thresholds: readonly number[];
5
+ detachStage?: number;
6
+ }
7
+ export interface DamageZoneState {
8
+ id: string;
9
+ accumulated: number;
10
+ stage: number;
11
+ detached: boolean;
12
+ }
13
+ export interface DamageTransition {
14
+ zone: string;
15
+ stage: number;
16
+ previousStage: number;
17
+ detached: boolean;
18
+ disabled: boolean;
19
+ accumulated: number;
20
+ total: number;
21
+ }
22
+ export interface DamageModelConfig {
23
+ zones: readonly DamageZoneDef[];
24
+ disableAt?: number;
25
+ onStage?: (transition: DamageTransition) => void;
26
+ onDetach?: (zoneId: string, accumulated: number) => void;
27
+ }
28
+ /**
29
+ * Coarse crash-damage model (issue #86 — stages, not soft-body). Each zone accumulates contact impulse
30
+ * fed from G3's `onCollision` `CollisionEvent`; crossing a threshold bumps the zone to the next discrete
31
+ * stage (the caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once,
32
+ * and a total-impulse `disableAt` flips the whole body to a disabled state. `absorb` returns a transition
33
+ * only when something actually changed, so it doubles as the event source for HUD/feed.
34
+ */
35
+ export declare class DamageModel {
36
+ private readonly zones;
37
+ private readonly order;
38
+ private readonly disableAt;
39
+ private readonly onStage?;
40
+ private readonly onDetach?;
41
+ private totalImpulse;
42
+ private disabledFlag;
43
+ constructor(config: DamageModelConfig);
44
+ get total(): number;
45
+ get disabled(): boolean;
46
+ stageOf(zoneId: string): number;
47
+ states(): readonly DamageZoneState[];
48
+ absorb(zoneId: string, impulse: number): DamageTransition | null;
49
+ /**
50
+ * Route a raw {@link CollisionEvent} into a zone by a caller-supplied selector (front/rear/side from
51
+ * `nx`/`nz`, or which body index took the hit). The event's `impulse` feeds `absorb`.
52
+ */
53
+ routeCollision(event: CollisionEvent, resolveZone: (event: CollisionEvent) => string | null): DamageTransition | null;
54
+ reset(): void;
55
+ }
56
+ export declare function createDamageModel(config: DamageModelConfig): DamageModel;
@@ -0,0 +1,107 @@
1
+ function stageFor(thresholds, accumulated) {
2
+ let stage = 0;
3
+ for (const t of thresholds) {
4
+ if (accumulated >= t)
5
+ stage += 1;
6
+ else
7
+ break;
8
+ }
9
+ return stage;
10
+ }
11
+ /**
12
+ * Coarse crash-damage model (issue #86 — stages, not soft-body). Each zone accumulates contact impulse
13
+ * fed from G3's `onCollision` `CollisionEvent`; crossing a threshold bumps the zone to the next discrete
14
+ * stage (the caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once,
15
+ * and a total-impulse `disableAt` flips the whole body to a disabled state. `absorb` returns a transition
16
+ * only when something actually changed, so it doubles as the event source for HUD/feed.
17
+ */
18
+ export class DamageModel {
19
+ zones = new Map();
20
+ order = [];
21
+ disableAt;
22
+ onStage;
23
+ onDetach;
24
+ totalImpulse = 0;
25
+ disabledFlag = false;
26
+ constructor(config) {
27
+ for (const def of config.zones) {
28
+ this.zones.set(def.id, { def, state: { id: def.id, accumulated: 0, stage: 0, detached: false } });
29
+ this.order.push(def.id);
30
+ }
31
+ this.disableAt = config.disableAt ?? null;
32
+ this.onStage = config.onStage;
33
+ this.onDetach = config.onDetach;
34
+ }
35
+ get total() {
36
+ return this.totalImpulse;
37
+ }
38
+ get disabled() {
39
+ return this.disabledFlag;
40
+ }
41
+ stageOf(zoneId) {
42
+ return this.zones.get(zoneId)?.state.stage ?? 0;
43
+ }
44
+ states() {
45
+ return this.order.map((id) => {
46
+ const s = this.zones.get(id).state;
47
+ return { id: s.id, accumulated: s.accumulated, stage: s.stage, detached: s.detached };
48
+ });
49
+ }
50
+ absorb(zoneId, impulse) {
51
+ const entry = this.zones.get(zoneId);
52
+ if (entry === undefined || impulse <= 0)
53
+ return null;
54
+ const { def, state } = entry;
55
+ const previousStage = state.stage;
56
+ const wasDisabled = this.disabledFlag;
57
+ state.accumulated += impulse;
58
+ this.totalImpulse += impulse;
59
+ state.stage = stageFor(def.thresholds, state.accumulated);
60
+ let detachedNow = false;
61
+ if (def.detachStage !== undefined &&
62
+ !state.detached &&
63
+ state.stage >= def.detachStage) {
64
+ state.detached = true;
65
+ detachedNow = true;
66
+ this.onDetach?.(zoneId, state.accumulated);
67
+ }
68
+ if (this.disableAt !== null && this.totalImpulse >= this.disableAt)
69
+ this.disabledFlag = true;
70
+ const changed = state.stage !== previousStage || detachedNow || this.disabledFlag !== wasDisabled;
71
+ if (!changed)
72
+ return null;
73
+ const transition = {
74
+ zone: zoneId,
75
+ stage: state.stage,
76
+ previousStage,
77
+ detached: state.detached,
78
+ disabled: this.disabledFlag,
79
+ accumulated: state.accumulated,
80
+ total: this.totalImpulse,
81
+ };
82
+ this.onStage?.(transition);
83
+ return transition;
84
+ }
85
+ /**
86
+ * Route a raw {@link CollisionEvent} into a zone by a caller-supplied selector (front/rear/side from
87
+ * `nx`/`nz`, or which body index took the hit). The event's `impulse` feeds `absorb`.
88
+ */
89
+ routeCollision(event, resolveZone) {
90
+ const zone = resolveZone(event);
91
+ if (zone === null)
92
+ return null;
93
+ return this.absorb(zone, Math.abs(event.impulse));
94
+ }
95
+ reset() {
96
+ for (const { state } of this.zones.values()) {
97
+ state.accumulated = 0;
98
+ state.stage = 0;
99
+ state.detached = false;
100
+ }
101
+ this.totalImpulse = 0;
102
+ this.disabledFlag = false;
103
+ }
104
+ }
105
+ export function createDamageModel(config) {
106
+ return new DamageModel(config);
107
+ }
@@ -0,0 +1,48 @@
1
+ import type { PhysicsBounds, PhysicsWorld } from "./physicsWorld.js";
2
+ export type ForceMode = "impulse" | "velocity" | "accelerate";
3
+ export interface ForceVolumeConfig {
4
+ /** Region a body's center must be inside to be affected. */
5
+ bounds: PhysicsBounds;
6
+ /** `impulse` adds to velocity, `velocity` sets it, `accelerate` adds force·dt each tick. */
7
+ force: readonly [number, number, number];
8
+ /** Default `impulse`. */
9
+ mode?: ForceMode;
10
+ /** Fire only when a body first enters (boost pad) rather than every tick inside (fan/wind). Default false. */
11
+ once?: boolean;
12
+ }
13
+ /**
14
+ * A trigger region that pushes bodies passing through it — boost pads (`impulse` + `once`),
15
+ * conveyors (`velocity`), fans/wind (`accelerate`). Call `apply` each tick; `once` mode fires only
16
+ * on entry by tracking membership between ticks.
17
+ */
18
+ export declare class ForceVolume {
19
+ readonly bounds: PhysicsBounds;
20
+ readonly mode: ForceMode;
21
+ readonly once: boolean;
22
+ private readonly fx;
23
+ private readonly fy;
24
+ private readonly fz;
25
+ private members;
26
+ private nextMembers;
27
+ constructor(config: ForceVolumeConfig);
28
+ apply(world: PhysicsWorld, dt: number): void;
29
+ }
30
+ export interface PlatformCarryConfig {
31
+ /** Vertical gap between a rider's base and the platform top counted as "standing on". Default 0.12. */
32
+ contactTolerance?: number;
33
+ }
34
+ /**
35
+ * Carries bodies standing on a moving platform by composing their transform with the platform's
36
+ * per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The
37
+ * platform is a body the game repositions each frame; riders are detected by overlap on its top face.
38
+ */
39
+ export declare class PlatformCarry {
40
+ private readonly world;
41
+ private readonly platform;
42
+ private readonly tolerance;
43
+ private prevX;
44
+ private prevY;
45
+ private prevZ;
46
+ constructor(world: PhysicsWorld, platform: number, config?: PlatformCarryConfig);
47
+ step(): void;
48
+ }
@@ -0,0 +1,119 @@
1
+ function inside(bounds, x, y, z) {
2
+ return (x >= bounds.min[0] &&
3
+ x <= bounds.max[0] &&
4
+ y >= bounds.min[1] &&
5
+ y <= bounds.max[1] &&
6
+ z >= bounds.min[2] &&
7
+ z <= bounds.max[2]);
8
+ }
9
+ /**
10
+ * A trigger region that pushes bodies passing through it — boost pads (`impulse` + `once`),
11
+ * conveyors (`velocity`), fans/wind (`accelerate`). Call `apply` each tick; `once` mode fires only
12
+ * on entry by tracking membership between ticks.
13
+ */
14
+ export class ForceVolume {
15
+ bounds;
16
+ mode;
17
+ once;
18
+ fx;
19
+ fy;
20
+ fz;
21
+ members = new Set();
22
+ nextMembers = new Set();
23
+ constructor(config) {
24
+ this.bounds = config.bounds;
25
+ this.mode = config.mode ?? "impulse";
26
+ this.once = config.once ?? false;
27
+ this.fx = config.force[0];
28
+ this.fy = config.force[1];
29
+ this.fz = config.force[2];
30
+ }
31
+ apply(world, dt) {
32
+ const next = this.nextMembers;
33
+ next.clear();
34
+ const high = world.highWater;
35
+ for (let i = 0; i < high; i += 1) {
36
+ if (world.invMass[i] === 0)
37
+ continue;
38
+ if (!inside(this.bounds, world.posX[i], world.posY[i], world.posZ[i]))
39
+ continue;
40
+ const wasInside = this.members.has(i);
41
+ next.add(i);
42
+ if (this.once && wasInside)
43
+ continue;
44
+ switch (this.mode) {
45
+ case "velocity":
46
+ world.velX[i] = this.fx;
47
+ world.velY[i] = this.fy;
48
+ world.velZ[i] = this.fz;
49
+ break;
50
+ case "accelerate":
51
+ world.velX[i] += this.fx * dt;
52
+ world.velY[i] += this.fy * dt;
53
+ world.velZ[i] += this.fz * dt;
54
+ break;
55
+ default:
56
+ world.velX[i] += this.fx;
57
+ world.velY[i] += this.fy;
58
+ world.velZ[i] += this.fz;
59
+ }
60
+ world.wake(i);
61
+ }
62
+ this.nextMembers = this.members;
63
+ this.members = next;
64
+ }
65
+ }
66
+ /**
67
+ * Carries bodies standing on a moving platform by composing their transform with the platform's
68
+ * per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The
69
+ * platform is a body the game repositions each frame; riders are detected by overlap on its top face.
70
+ */
71
+ export class PlatformCarry {
72
+ world;
73
+ platform;
74
+ tolerance;
75
+ prevX;
76
+ prevY;
77
+ prevZ;
78
+ constructor(world, platform, config = {}) {
79
+ this.world = world;
80
+ this.platform = platform;
81
+ this.tolerance = config.contactTolerance ?? 0.12;
82
+ this.prevX = world.posX[platform];
83
+ this.prevY = world.posY[platform];
84
+ this.prevZ = world.posZ[platform];
85
+ }
86
+ step() {
87
+ const w = this.world;
88
+ const p = this.platform;
89
+ const dx = w.posX[p] - this.prevX;
90
+ const dy = w.posY[p] - this.prevY;
91
+ const dz = w.posZ[p] - this.prevZ;
92
+ const topY = this.prevY + w.halfY[p];
93
+ const px = this.prevX;
94
+ const pz = this.prevZ;
95
+ this.prevX = w.posX[p];
96
+ this.prevY = w.posY[p];
97
+ this.prevZ = w.posZ[p];
98
+ if (dx === 0 && dy === 0 && dz === 0)
99
+ return;
100
+ const hx = w.halfX[p];
101
+ const hz = w.halfZ[p];
102
+ const high = w.highWater;
103
+ for (let i = 0; i < high; i += 1) {
104
+ if (i === p || !w.isAlive(i))
105
+ continue;
106
+ const base = w.posY[i] - w.halfY[i];
107
+ if (base < topY - this.tolerance || base > topY + this.tolerance)
108
+ continue;
109
+ if (Math.abs(w.posX[i] - px) > hx + w.halfX[i])
110
+ continue;
111
+ if (Math.abs(w.posZ[i] - pz) > hz + w.halfZ[i])
112
+ continue;
113
+ w.posX[i] += dx;
114
+ w.posY[i] += dy;
115
+ w.posZ[i] += dz;
116
+ w.wake(i);
117
+ }
118
+ }
119
+ }
@@ -35,6 +35,10 @@ export interface PhysicsWorldConfig {
35
35
  sleepThresholdSteps?: number;
36
36
  /** Approach speed a contact must exceed to wake a sleeping body (resting contacts don't). Default 0.5. */
37
37
  wakeThreshold?: number;
38
+ /** Maximum simultaneous joints the constraint buffers can hold. Default 256. */
39
+ jointCapacity?: number;
40
+ /** Baumgarte fraction applied to hard-joint positional error per iteration, 0..1. Default 0.5. */
41
+ jointCorrection?: number;
38
42
  }
39
43
  export interface PhysicsStats {
40
44
  count: number;
@@ -45,9 +49,8 @@ export interface PhysicsStats {
45
49
  substeps: number;
46
50
  stepMs: number;
47
51
  }
48
- export interface AddBodyOptions {
52
+ export interface BodyCommonOptions {
49
53
  position: readonly [number, number, number];
50
- halfExtents: readonly [number, number, number];
51
54
  velocity?: readonly [number, number, number];
52
55
  /** ≤ 0 or omitted with `static` → infinite mass (immovable collider). Default 1. */
53
56
  mass?: number;
@@ -56,6 +59,56 @@ export interface AddBodyOptions {
56
59
  /** Seed the body asleep (a settled bed pays no integration cost until woken). */
57
60
  asleep?: boolean;
58
61
  }
62
+ export interface BoxBodyOptions extends BodyCommonOptions {
63
+ shape?: "box";
64
+ halfExtents: readonly [number, number, number];
65
+ }
66
+ /** The radius fills all three half-extent columns, so broadphase and bounds see the sphere's enclosing AABB. */
67
+ export interface SphereBodyOptions extends BodyCommonOptions {
68
+ shape: "sphere";
69
+ radius: number;
70
+ }
71
+ export type AddBodyOptions = BoxBodyOptions | SphereBodyOptions;
72
+ export declare const SHAPE_BOX = 0;
73
+ export declare const SHAPE_SPHERE = 1;
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).
78
+ */
79
+ export type JointKind = "hinge" | "distance" | "spring" | "fixed";
80
+ export interface JointOptions {
81
+ /** First body index. */
82
+ bodyA: number;
83
+ /** Second body index, or omit / < 0 to anchor A to a fixed world point (`anchorB`). */
84
+ bodyB?: number;
85
+ /** Local offset of the anchor on A, in world axes (no rotation). Default [0,0,0]. */
86
+ anchorA?: readonly [number, number, number];
87
+ /** Local offset on B, or — when B is a world anchor — the world point itself. Default [0,0,0]. */
88
+ anchorB?: readonly [number, number, number];
89
+ /** Target separation for `distance`/`spring`. Defaults to the anchors' current distance. */
90
+ restLength?: number;
91
+ /** Spring restoring rate. Default 40. */
92
+ stiffness?: number;
93
+ /** Spring damping (relative-velocity bleed). Default 6. */
94
+ damping?: number;
95
+ /** Clamp on the per-substep spring impulse (a follow-point that never yanks). Default unbounded. */
96
+ maxImpulse?: number;
97
+ /** Hinge axis, retained as metadata (this sim has no angular DOF). */
98
+ axis?: readonly [number, number, number];
99
+ }
100
+ /** A contact reported to `onCollision`. The object is reused each call — read/copy, never retain. */
101
+ export interface CollisionEvent {
102
+ a: number;
103
+ b: number;
104
+ nx: number;
105
+ ny: number;
106
+ nz: number;
107
+ approachSpeed: number;
108
+ impulse: number;
109
+ }
110
+ /** Cap on `nx*ny*nz` broadphase cells — guards a huge-bounds/tiny-cellSize config from hanging `step()`. */
111
+ export declare const MAX_BROADPHASE_CELLS = 1000000;
59
112
  /** Grid cell containing a point, clamped into the grid. Pure — exported for tests. */
60
113
  export declare function cellCoord(value: number, min: number, cellSize: number, cells: number): number;
61
114
  /** Linear cell index from clamped 3D coords. Pure — exported for tests. */
@@ -77,6 +130,8 @@ export declare class PhysicsWorld {
77
130
  readonly sleepMove2: number;
78
131
  readonly sleepSteps: number;
79
132
  readonly wakeThreshold: number;
133
+ readonly jointCapacity: number;
134
+ readonly jointCorrection: number;
80
135
  readonly posX: Float32Array;
81
136
  readonly posY: Float32Array;
82
137
  readonly posZ: Float32Array;
@@ -88,6 +143,8 @@ export declare class PhysicsWorld {
88
143
  readonly halfZ: Float32Array;
89
144
  readonly invMass: Float32Array;
90
145
  readonly flags: Uint8Array;
146
+ /** `SHAPE_BOX` (0) or `SHAPE_SPHERE` (1) per body; spheres keep their radius in halfX/halfY/halfZ. */
147
+ readonly shape: Uint8Array;
91
148
  /** 1 for a body that took part in a contact this frame; reset each `step()`. Render tint reads this. */
92
149
  readonly contact: Uint8Array;
93
150
  private readonly sleepTimer;
@@ -105,6 +162,30 @@ export declare class PhysicsWorld {
105
162
  private readonly awakeList;
106
163
  private readonly awakeSlot;
107
164
  private awakeCount;
165
+ private readonly jSpring;
166
+ private readonly jActive;
167
+ private readonly jBodyA;
168
+ private readonly jBodyB;
169
+ private readonly jAnchorAX;
170
+ private readonly jAnchorAY;
171
+ private readonly jAnchorAZ;
172
+ private readonly jAnchorBX;
173
+ private readonly jAnchorBY;
174
+ private readonly jAnchorBZ;
175
+ private readonly jRest;
176
+ private readonly jStiffness;
177
+ private readonly jDamping;
178
+ private readonly jMaxImpulse;
179
+ private readonly jFree;
180
+ private jFreeCount;
181
+ private jointHigh;
182
+ private jointActiveCount;
183
+ private readonly bodyFree;
184
+ private bodyFreeCount;
185
+ private bodyHigh;
186
+ private collisionListener;
187
+ private collisionMinSpeed;
188
+ private readonly collisionEvent;
108
189
  private bodyCount;
109
190
  private accumulator;
110
191
  private readonly stats;
@@ -112,15 +193,55 @@ export declare class PhysicsWorld {
112
193
  private addAwake;
113
194
  private removeAwake;
114
195
  get count(): number;
196
+ /** One past the highest slot `addBody` has ever handed out — the range `removeBody` leaves holes in. */
197
+ get highWater(): number;
115
198
  get cells(): {
116
199
  nx: number;
117
200
  ny: number;
118
201
  nz: number;
119
202
  total: number;
120
203
  };
204
+ get jointCount(): number;
205
+ /**
206
+ * Deliver every impacting contact (approach speed ≥ `minApproachSpeed`) to `listener` during
207
+ * `step`. The event object is reused — read/copy it, never retain. Pass `null` to detach. This is
208
+ * the seam crash-damage and destruction read; contacts otherwise stay inside the sim.
209
+ */
210
+ onCollision(listener: ((e: CollisionEvent) => void) | null, minApproachSpeed?: number): void;
211
+ hingeJoint(options: JointOptions): number;
212
+ fixedJoint(options: JointOptions): number;
213
+ distanceJoint(options: JointOptions): number;
214
+ springJoint(options: JointOptions): number;
215
+ private addJoint;
216
+ removeJoint(id: number): void;
217
+ /** Move a joint's world anchor point (only meaningful when `bodyB` is a world anchor). */
218
+ setJointAnchor(id: number, x: number, y: number, z: number): void;
219
+ /** Move a joint's local anchor offset on A (world axes, no rotation applied by the solver). */
220
+ setJointAnchorA(id: number, x: number, y: number, z: number): void;
221
+ setJointRest(id: number, restLength: number): void;
222
+ private jointDistance;
223
+ /** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
224
+ readJointSegments(out: Float32Array): number;
121
225
  addBody(options: AddBodyOptions): number;
226
+ /**
227
+ * Remove a body: it drops out of integration, the solver, and the broadphase, and its slot is
228
+ * queued for the next `addBody` to reuse. `id` and every other body's `id` stay valid — a dead
229
+ * slot is tombstoned (`FLAG_DEAD`), never swapped, since ids are handed out raw and stored as-is
230
+ * in joints and game state. There is no persistent contact set to consult, so — conservatively —
231
+ * any sleeping body whose AABB touches the removed body's (padded by `slop`) is woken; a body
232
+ * that was resting on it always qualifies.
233
+ */
234
+ removeBody(id: number): void;
235
+ private wakeTouching;
122
236
  isSleeping(i: number): boolean;
237
+ isAlive(i: number): boolean;
123
238
  wake(i: number): void;
239
+ /** Overwrite a body's velocity directly, waking it if asleep. */
240
+ setVelocity(id: number, x: number, y: number, z: number): void;
241
+ /** Move a body, keeping its current velocity, and wake it if asleep. */
242
+ setPosition(id: number, x: number, y: number, z: number): void;
243
+ /** Move a body and zero its velocity — a hard reset (respawn, teleporter) instead of a slide. */
244
+ teleport(id: number, x: number, y: number, z: number): void;
124
245
  wakeAll(): void;
125
246
  clear(): void;
126
247
  /** Advance by a real frame delta; runs whole fixed substeps and carries the remainder. */
@@ -132,5 +253,6 @@ export declare class PhysicsWorld {
132
253
  private cellOf;
133
254
  private solve;
134
255
  private resolve;
256
+ private solveJoints;
135
257
  private updateSleep;
136
258
  }