@jgengine/core 0.6.0 → 0.7.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 (222) hide show
  1. package/CHANGELOG.md +167 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +2 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +281 -4
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +35 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +83 -0
  116. package/dist/physics/physicsWorld.js +261 -1
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +64 -2
  128. package/dist/runtime/gameContext.js +154 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/autoTarget.d.ts +15 -0
  134. package/dist/scene/autoTarget.js +54 -0
  135. package/dist/scene/behaviors.d.ts +13 -1
  136. package/dist/scene/behaviors.js +3 -0
  137. package/dist/scene/captureCheck.d.ts +7 -0
  138. package/dist/scene/captureCheck.js +11 -0
  139. package/dist/scene/entityStore.d.ts +1 -1
  140. package/dist/scene/form.d.ts +40 -0
  141. package/dist/scene/form.js +56 -0
  142. package/dist/scene/mount.d.ts +62 -0
  143. package/dist/scene/mount.js +107 -0
  144. package/dist/scene/possession.d.ts +30 -0
  145. package/dist/scene/possession.js +51 -0
  146. package/dist/scene/roster.d.ts +25 -0
  147. package/dist/scene/roster.js +57 -0
  148. package/dist/scene/selection.d.ts +30 -0
  149. package/dist/scene/selection.js +38 -0
  150. package/dist/scene/stationClaim.d.ts +43 -0
  151. package/dist/scene/stationClaim.js +109 -0
  152. package/dist/sensor/frustumSensor.d.ts +60 -0
  153. package/dist/sensor/frustumSensor.js +104 -0
  154. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  155. package/dist/sensor/hiddenStateProbe.js +40 -0
  156. package/dist/sensor/recordingBuffer.d.ts +29 -0
  157. package/dist/sensor/recordingBuffer.js +61 -0
  158. package/dist/sensor/revealQuery.d.ts +22 -0
  159. package/dist/sensor/revealQuery.js +36 -0
  160. package/dist/session/contestedChannel.d.ts +36 -0
  161. package/dist/session/contestedChannel.js +100 -0
  162. package/dist/session/extraction.d.ts +54 -0
  163. package/dist/session/extraction.js +108 -0
  164. package/dist/session/ring.d.ts +36 -0
  165. package/dist/session/ring.js +88 -0
  166. package/dist/session/roundState.d.ts +55 -0
  167. package/dist/session/roundState.js +129 -0
  168. package/dist/stats/accumulatorMeter.d.ts +31 -0
  169. package/dist/stats/accumulatorMeter.js +81 -0
  170. package/dist/stats/eventMeter.d.ts +34 -0
  171. package/dist/stats/eventMeter.js +76 -0
  172. package/dist/stats/rollCheck.d.ts +15 -0
  173. package/dist/stats/rollCheck.js +15 -0
  174. package/dist/survival/decayMeter.d.ts +59 -0
  175. package/dist/survival/decayMeter.js +95 -0
  176. package/dist/survival/moodle.d.ts +48 -0
  177. package/dist/survival/moodle.js +91 -0
  178. package/dist/survival/regionHealth.d.ts +72 -0
  179. package/dist/survival/regionHealth.js +129 -0
  180. package/dist/tactics/predictiveQuery.d.ts +34 -0
  181. package/dist/tactics/predictiveQuery.js +44 -0
  182. package/dist/tactics/snapshot.d.ts +18 -0
  183. package/dist/tactics/snapshot.js +65 -0
  184. package/dist/tactics/surface.d.ts +52 -0
  185. package/dist/tactics/surface.js +135 -0
  186. package/dist/tactics/tacticalGrid.d.ts +57 -0
  187. package/dist/tactics/tacticalGrid.js +244 -0
  188. package/dist/time/beatClock.d.ts +40 -0
  189. package/dist/time/beatClock.js +73 -0
  190. package/dist/turn/commit.d.ts +27 -0
  191. package/dist/turn/commit.js +72 -0
  192. package/dist/turn/turnLoop.d.ts +58 -0
  193. package/dist/turn/turnLoop.js +216 -0
  194. package/dist/world/buildPermissions.d.ts +47 -0
  195. package/dist/world/buildPermissions.js +134 -0
  196. package/dist/world/carve.d.ts +105 -0
  197. package/dist/world/carve.js +175 -0
  198. package/dist/world/connectors.d.ts +40 -0
  199. package/dist/world/connectors.js +67 -0
  200. package/dist/world/envField.d.ts +71 -0
  201. package/dist/world/envField.js +96 -0
  202. package/dist/world/fog.d.ts +40 -0
  203. package/dist/world/fog.js +137 -0
  204. package/dist/world/markers.d.ts +52 -0
  205. package/dist/world/markers.js +96 -0
  206. package/dist/world/minimap.d.ts +42 -0
  207. package/dist/world/minimap.js +72 -0
  208. package/dist/world/placedStructureStore.d.ts +39 -0
  209. package/dist/world/placedStructureStore.js +95 -0
  210. package/dist/world/placementController.d.ts +48 -0
  211. package/dist/world/placementController.js +97 -0
  212. package/dist/world/realm.d.ts +61 -0
  213. package/dist/world/realm.js +66 -0
  214. package/dist/world/support.d.ts +27 -0
  215. package/dist/world/support.js +75 -0
  216. package/dist/world/terraform.d.ts +54 -0
  217. package/dist/world/terraform.js +186 -0
  218. package/dist/world/walls.d.ts +61 -0
  219. package/dist/world/walls.js +221 -0
  220. package/dist/world/weather.d.ts +92 -0
  221. package/dist/world/weather.js +169 -0
  222. package/package.json +1 -1
@@ -26,6 +26,8 @@ export class PhysicsWorld {
26
26
  sleepMove2;
27
27
  sleepSteps;
28
28
  wakeThreshold;
29
+ jointCapacity;
30
+ jointCorrection;
29
31
  posX;
30
32
  posY;
31
33
  posZ;
@@ -56,6 +58,35 @@ export class PhysicsWorld {
56
58
  awakeList;
57
59
  awakeSlot;
58
60
  awakeCount = 0;
61
+ jSpring;
62
+ jActive;
63
+ jBodyA;
64
+ jBodyB;
65
+ jAnchorAX;
66
+ jAnchorAY;
67
+ jAnchorAZ;
68
+ jAnchorBX;
69
+ jAnchorBY;
70
+ jAnchorBZ;
71
+ jRest;
72
+ jStiffness;
73
+ jDamping;
74
+ jMaxImpulse;
75
+ jFree;
76
+ jFreeCount = 0;
77
+ jointHigh = 0;
78
+ jointActiveCount = 0;
79
+ collisionListener = null;
80
+ collisionMinSpeed = 0;
81
+ collisionEvent = {
82
+ a: 0,
83
+ b: 0,
84
+ nx: 0,
85
+ ny: 0,
86
+ nz: 0,
87
+ approachSpeed: 0,
88
+ impulse: 0,
89
+ };
59
90
  bodyCount = 0;
60
91
  accumulator = 0;
61
92
  stats = {
@@ -86,6 +117,8 @@ export class PhysicsWorld {
86
117
  this.sleepMove2 = sleepMoveEps * sleepMoveEps;
87
118
  this.sleepSteps = config.sleepThresholdSteps ?? 30;
88
119
  this.wakeThreshold = config.wakeThreshold ?? 0.5;
120
+ this.jointCapacity = Math.max(0, config.jointCapacity ?? 256);
121
+ this.jointCorrection = config.jointCorrection ?? 0.5;
89
122
  const cap = config.capacity;
90
123
  this.posX = new Float32Array(cap);
91
124
  this.posY = new Float32Array(cap);
@@ -114,6 +147,22 @@ export class PhysicsWorld {
114
147
  this.bodyCell = new Int32Array(cap);
115
148
  this.awakeList = new Int32Array(cap);
116
149
  this.awakeSlot = new Int32Array(cap).fill(-1);
150
+ const jc = this.jointCapacity;
151
+ this.jSpring = new Uint8Array(jc);
152
+ this.jActive = new Uint8Array(jc);
153
+ this.jBodyA = new Int32Array(jc);
154
+ this.jBodyB = new Int32Array(jc);
155
+ this.jAnchorAX = new Float32Array(jc);
156
+ this.jAnchorAY = new Float32Array(jc);
157
+ this.jAnchorAZ = new Float32Array(jc);
158
+ this.jAnchorBX = new Float32Array(jc);
159
+ this.jAnchorBY = new Float32Array(jc);
160
+ this.jAnchorBZ = new Float32Array(jc);
161
+ this.jRest = new Float32Array(jc);
162
+ this.jStiffness = new Float32Array(jc);
163
+ this.jDamping = new Float32Array(jc);
164
+ this.jMaxImpulse = new Float32Array(jc);
165
+ this.jFree = new Int32Array(jc);
117
166
  }
118
167
  addAwake(i) {
119
168
  if (this.awakeSlot[i] >= 0)
@@ -138,6 +187,109 @@ export class PhysicsWorld {
138
187
  get cells() {
139
188
  return { nx: this.nx, ny: this.ny, nz: this.nz, total: this.numCells };
140
189
  }
190
+ get jointCount() {
191
+ return this.jointActiveCount;
192
+ }
193
+ /**
194
+ * Deliver every impacting contact (approach speed ≥ `minApproachSpeed`) to `listener` during
195
+ * `step`. The event object is reused — read/copy it, never retain. Pass `null` to detach. This is
196
+ * the seam crash-damage and destruction read; contacts otherwise stay inside the sim.
197
+ */
198
+ onCollision(listener, minApproachSpeed = 0) {
199
+ this.collisionListener = listener;
200
+ this.collisionMinSpeed = minApproachSpeed;
201
+ }
202
+ hingeJoint(options) {
203
+ return this.addJoint(options, false, 0);
204
+ }
205
+ fixedJoint(options) {
206
+ return this.addJoint(options, false, 0);
207
+ }
208
+ distanceJoint(options) {
209
+ return this.addJoint(options, false, null);
210
+ }
211
+ springJoint(options) {
212
+ return this.addJoint(options, true, null);
213
+ }
214
+ addJoint(options, spring, restOverride) {
215
+ let id;
216
+ if (this.jFreeCount > 0) {
217
+ id = this.jFree[--this.jFreeCount];
218
+ }
219
+ else {
220
+ if (this.jointHigh >= this.jointCapacity)
221
+ throw new Error("PhysicsWorld: joint capacity exceeded");
222
+ id = this.jointHigh++;
223
+ }
224
+ const bodyB = options.bodyB ?? -1;
225
+ const aA = options.anchorA;
226
+ const aB = options.anchorB;
227
+ this.jBodyA[id] = options.bodyA;
228
+ this.jBodyB[id] = bodyB;
229
+ this.jAnchorAX[id] = aA?.[0] ?? 0;
230
+ this.jAnchorAY[id] = aA?.[1] ?? 0;
231
+ this.jAnchorAZ[id] = aA?.[2] ?? 0;
232
+ this.jAnchorBX[id] = aB?.[0] ?? 0;
233
+ this.jAnchorBY[id] = aB?.[1] ?? 0;
234
+ this.jAnchorBZ[id] = aB?.[2] ?? 0;
235
+ this.jSpring[id] = spring ? 1 : 0;
236
+ this.jStiffness[id] = options.stiffness ?? 40;
237
+ this.jDamping[id] = options.damping ?? 6;
238
+ this.jMaxImpulse[id] = options.maxImpulse ?? Number.POSITIVE_INFINITY;
239
+ this.jRest[id] = restOverride ?? options.restLength ?? this.jointDistance(id);
240
+ this.jActive[id] = 1;
241
+ this.jointActiveCount += 1;
242
+ return id;
243
+ }
244
+ removeJoint(id) {
245
+ if (this.jActive[id] !== 1)
246
+ return;
247
+ this.jActive[id] = 0;
248
+ this.jFree[this.jFreeCount++] = id;
249
+ this.jointActiveCount -= 1;
250
+ }
251
+ /** Move a joint's world anchor point (only meaningful when `bodyB` is a world anchor). */
252
+ setJointAnchor(id, x, y, z) {
253
+ this.jAnchorBX[id] = x;
254
+ this.jAnchorBY[id] = y;
255
+ this.jAnchorBZ[id] = z;
256
+ }
257
+ setJointRest(id, restLength) {
258
+ this.jRest[id] = restLength;
259
+ }
260
+ jointDistance(id) {
261
+ const bA = this.jBodyA[id];
262
+ const bB = this.jBodyB[id];
263
+ const ax = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
264
+ const ay = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
265
+ const az = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
266
+ const bx = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
267
+ const by = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
268
+ const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
269
+ const dx = bx - ax;
270
+ const dy = by - ay;
271
+ const dz = bz - az;
272
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
273
+ }
274
+ /** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
275
+ readJointSegments(out) {
276
+ let n = 0;
277
+ for (let id = 0; id < this.jointHigh; id += 1) {
278
+ if (this.jActive[id] !== 1)
279
+ continue;
280
+ const bA = this.jBodyA[id];
281
+ const bB = this.jBodyB[id];
282
+ const o = n * 6;
283
+ out[o] = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
284
+ out[o + 1] = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
285
+ out[o + 2] = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
286
+ out[o + 3] = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
287
+ out[o + 4] = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
288
+ out[o + 5] = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
289
+ n += 1;
290
+ }
291
+ return n;
292
+ }
141
293
  addBody(options) {
142
294
  if (this.bodyCount >= this.capacity)
143
295
  throw new Error("PhysicsWorld: capacity exceeded");
@@ -194,6 +346,9 @@ export class PhysicsWorld {
194
346
  this.bodyCount = 0;
195
347
  this.accumulator = 0;
196
348
  this.awakeCount = 0;
349
+ this.jointHigh = 0;
350
+ this.jFreeCount = 0;
351
+ this.jointActiveCount = 0;
197
352
  }
198
353
  /** Advance by a real frame delta; runs whole fixed substeps and carries the remainder. */
199
354
  step(frameDt) {
@@ -243,8 +398,11 @@ export class PhysicsWorld {
243
398
  }
244
399
  this.buildGrid();
245
400
  const iterations = this.solverIterations;
246
- for (let it = 0; it < iterations; it += 1)
401
+ for (let it = 0; it < iterations; it += 1) {
247
402
  this.solve(it === 0, it === iterations - 1);
403
+ if (this.jointActiveCount > 0)
404
+ this.solveJoints(dt);
405
+ }
248
406
  for (let a = 0; a < this.awakeCount; a += 1)
249
407
  this.constrainBounds(this.awakeList[a]);
250
408
  this.updateSleep();
@@ -416,6 +574,17 @@ export class PhysicsWorld {
416
574
  if (rvn < 0) {
417
575
  const e = restitutionPass && -rvn > this.restitutionThreshold ? this.restitution : 0;
418
576
  const jn = (-(1 + e) * rvn) / imSum;
577
+ if (restitutionPass && this.collisionListener !== null && -rvn >= this.collisionMinSpeed) {
578
+ const ev = this.collisionEvent;
579
+ ev.a = i;
580
+ ev.b = j;
581
+ ev.nx = nX;
582
+ ev.ny = nY;
583
+ ev.nz = nZ;
584
+ ev.approachSpeed = -rvn;
585
+ ev.impulse = jn;
586
+ this.collisionListener(ev);
587
+ }
419
588
  this.velX[i] -= jn * imI * nX;
420
589
  this.velY[i] -= jn * imI * nY;
421
590
  this.velZ[i] -= jn * imI * nZ;
@@ -452,6 +621,97 @@ export class PhysicsWorld {
452
621
  this.posY[j] += corr * imJ * nY;
453
622
  this.posZ[j] += corr * imJ * nZ;
454
623
  }
624
+ solveJoints(dt) {
625
+ const beta = this.jointCorrection;
626
+ const slop = this.slop;
627
+ for (let id = 0; id < this.jointHigh; id += 1) {
628
+ if (this.jActive[id] !== 1)
629
+ continue;
630
+ const bA = this.jBodyA[id];
631
+ const bB = this.jBodyB[id];
632
+ const ax = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
633
+ const ay = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
634
+ const az = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
635
+ const bx = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
636
+ const by = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
637
+ const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
638
+ const dx = bx - ax;
639
+ const dy = by - ay;
640
+ const dz = bz - az;
641
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
642
+ if (dist < 1e-9)
643
+ continue;
644
+ const nX = dx / dist;
645
+ const nY = dy / dist;
646
+ const nZ = dz / dist;
647
+ const c = dist - this.jRest[id];
648
+ const imA = bA >= 0 ? this.invMass[bA] : 0;
649
+ const imB = bB >= 0 ? this.invMass[bB] : 0;
650
+ const imSum = imA + imB;
651
+ if (imSum === 0)
652
+ continue;
653
+ const vax = bA >= 0 ? this.velX[bA] : 0;
654
+ const vay = bA >= 0 ? this.velY[bA] : 0;
655
+ const vaz = bA >= 0 ? this.velZ[bA] : 0;
656
+ const vbx = bB >= 0 ? this.velX[bB] : 0;
657
+ const vby = bB >= 0 ? this.velY[bB] : 0;
658
+ const vbz = bB >= 0 ? this.velZ[bB] : 0;
659
+ const rvn = (vbx - vax) * nX + (vby - vay) * nY + (vbz - vaz) * nZ;
660
+ if (this.jSpring[id] === 1) {
661
+ if (Math.abs(c) > slop) {
662
+ if (bA >= 0)
663
+ this.wake(bA);
664
+ if (bB >= 0)
665
+ this.wake(bB);
666
+ }
667
+ let jn = (-(this.jStiffness[id] * c + this.jDamping[id] * rvn) * dt) / imSum;
668
+ const maxJ = this.jMaxImpulse[id];
669
+ if (jn > maxJ)
670
+ jn = maxJ;
671
+ else if (jn < -maxJ)
672
+ jn = -maxJ;
673
+ if (bA >= 0) {
674
+ this.velX[bA] -= jn * imA * nX;
675
+ this.velY[bA] -= jn * imA * nY;
676
+ this.velZ[bA] -= jn * imA * nZ;
677
+ }
678
+ if (bB >= 0) {
679
+ this.velX[bB] += jn * imB * nX;
680
+ this.velY[bB] += jn * imB * nY;
681
+ this.velZ[bB] += jn * imB * nZ;
682
+ }
683
+ continue;
684
+ }
685
+ if (Math.abs(c) > slop) {
686
+ if (bA >= 0)
687
+ this.wake(bA);
688
+ if (bB >= 0)
689
+ this.wake(bB);
690
+ }
691
+ const jn = -rvn / imSum;
692
+ if (bA >= 0) {
693
+ this.velX[bA] -= jn * imA * nX;
694
+ this.velY[bA] -= jn * imA * nY;
695
+ this.velZ[bA] -= jn * imA * nZ;
696
+ }
697
+ if (bB >= 0) {
698
+ this.velX[bB] += jn * imB * nX;
699
+ this.velY[bB] += jn * imB * nY;
700
+ this.velZ[bB] += jn * imB * nZ;
701
+ }
702
+ const corr = (c * beta) / imSum;
703
+ if (bA >= 0) {
704
+ this.posX[bA] += corr * imA * nX;
705
+ this.posY[bA] += corr * imA * nY;
706
+ this.posZ[bA] += corr * imA * nZ;
707
+ }
708
+ if (bB >= 0) {
709
+ this.posX[bB] -= corr * imB * nX;
710
+ this.posY[bB] -= corr * imB * nY;
711
+ this.posZ[bB] -= corr * imB * nZ;
712
+ }
713
+ }
714
+ }
455
715
  updateSleep() {
456
716
  // A body sleeps when it has actually stopped moving — measured by displacement over
457
717
  // the substep, not instantaneous velocity, which can stay "stuck" nonzero on a body
@@ -0,0 +1,49 @@
1
+ import type { PhysicsWorld } from "./physicsWorld.js";
2
+ export interface RagdollBoneSpec {
3
+ name: string;
4
+ position: readonly [number, number, number];
5
+ halfExtents: readonly [number, number, number];
6
+ /** Default 1. */
7
+ mass?: number;
8
+ }
9
+ export interface RagdollLinkSpec {
10
+ a: string;
11
+ b: string;
12
+ /** `hinge` pins the shared anchor; `distance` (default) holds the bones' current separation. */
13
+ kind?: "hinge" | "distance";
14
+ /** World point for a hinge pin. Defaults to the midpoint of the two bone centers. */
15
+ anchor?: readonly [number, number, number];
16
+ }
17
+ export interface RagdollBalanceConfig {
18
+ /** Bone the motor keeps upright (torso/head). */
19
+ root: string;
20
+ /** Target height of the root above the floor the motor drives toward. */
21
+ targetHeight: number;
22
+ /** Corrective gain pulling the root back to `targetHeight`. Default 8. */
23
+ strength?: number;
24
+ /** Locomotion push applied to the root from the move vector (active-ragdoll walking). Default 6. */
25
+ moveForce?: number;
26
+ }
27
+ export interface RagdollConfig {
28
+ bones: RagdollBoneSpec[];
29
+ links: RagdollLinkSpec[];
30
+ /** Optional active-ragdoll balance motor; omit for a purely floppy ragdoll. */
31
+ balance?: RagdollBalanceConfig;
32
+ }
33
+ /**
34
+ * A jointed multi-body character — floppy by default, or active-ragdoll when a balance motor is
35
+ * configured. Bones are `PhysicsWorld` bodies; links are joints on the joint API. `balance(dt, mx, mz)`
36
+ * drives the root toward its target height (staying upright) and pushes it by the move vector.
37
+ */
38
+ export declare class Ragdoll {
39
+ readonly bones: Record<string, number>;
40
+ readonly links: number[];
41
+ private readonly world;
42
+ private readonly config?;
43
+ constructor(world: PhysicsWorld, bones: Record<string, number>, links: number[], balance?: RagdollBalanceConfig);
44
+ centerOfMass(): [number, number, number];
45
+ applyImpulse(bone: string, x: number, y: number, z: number): void;
46
+ balance(dt: number, moveX?: number, moveZ?: number): void;
47
+ remove(): void;
48
+ }
49
+ export declare function createRagdoll(world: PhysicsWorld, config: RagdollConfig): Ragdoll;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * A jointed multi-body character — floppy by default, or active-ragdoll when a balance motor is
3
+ * configured. Bones are `PhysicsWorld` bodies; links are joints on the joint API. `balance(dt, mx, mz)`
4
+ * drives the root toward its target height (staying upright) and pushes it by the move vector.
5
+ */
6
+ export class Ragdoll {
7
+ bones;
8
+ links;
9
+ world;
10
+ config;
11
+ constructor(world, bones, links, balance) {
12
+ this.world = world;
13
+ this.bones = bones;
14
+ this.links = links;
15
+ this.config = balance;
16
+ }
17
+ centerOfMass() {
18
+ const w = this.world;
19
+ let mx = 0;
20
+ let my = 0;
21
+ let mz = 0;
22
+ let total = 0;
23
+ for (const id of Object.values(this.bones)) {
24
+ const im = w.invMass[id];
25
+ const m = im > 0 ? 1 / im : 0;
26
+ mx += w.posX[id] * m;
27
+ my += w.posY[id] * m;
28
+ mz += w.posZ[id] * m;
29
+ total += m;
30
+ }
31
+ if (total === 0)
32
+ return [0, 0, 0];
33
+ return [mx / total, my / total, mz / total];
34
+ }
35
+ applyImpulse(bone, x, y, z) {
36
+ const w = this.world;
37
+ const id = this.bones[bone];
38
+ if (id === undefined)
39
+ return;
40
+ const im = w.invMass[id];
41
+ w.velX[id] += x * im;
42
+ w.velY[id] += y * im;
43
+ w.velZ[id] += z * im;
44
+ w.wake(id);
45
+ }
46
+ balance(dt, moveX = 0, moveZ = 0) {
47
+ const c = this.config;
48
+ if (c === undefined)
49
+ return;
50
+ const w = this.world;
51
+ const id = this.bones[c.root];
52
+ if (id === undefined)
53
+ return;
54
+ const strength = c.strength ?? 8;
55
+ const moveForce = c.moveForce ?? 6;
56
+ const err = c.targetHeight - w.posY[id];
57
+ w.velY[id] += err * strength * dt;
58
+ w.velX[id] += moveX * moveForce * dt;
59
+ w.velZ[id] += moveZ * moveForce * dt;
60
+ w.wake(id);
61
+ }
62
+ remove() {
63
+ for (const j of this.links)
64
+ this.world.removeJoint(j);
65
+ this.links.length = 0;
66
+ }
67
+ }
68
+ export function createRagdoll(world, config) {
69
+ const bones = {};
70
+ for (const spec of config.bones) {
71
+ bones[spec.name] = world.addBody({
72
+ position: spec.position,
73
+ halfExtents: spec.halfExtents,
74
+ mass: spec.mass ?? 1,
75
+ });
76
+ }
77
+ const links = [];
78
+ for (const link of config.links) {
79
+ const a = bones[link.a];
80
+ const b = bones[link.b];
81
+ if (a === undefined || b === undefined)
82
+ continue;
83
+ if ((link.kind ?? "distance") === "hinge") {
84
+ const mid = link.anchor !== undefined
85
+ ? [link.anchor[0], link.anchor[1], link.anchor[2]]
86
+ : [
87
+ (world.posX[a] + world.posX[b]) / 2,
88
+ (world.posY[a] + world.posY[b]) / 2,
89
+ (world.posZ[a] + world.posZ[b]) / 2,
90
+ ];
91
+ links.push(world.hingeJoint({
92
+ bodyA: a,
93
+ bodyB: b,
94
+ anchorA: [mid[0] - world.posX[a], mid[1] - world.posY[a], mid[2] - world.posZ[a]],
95
+ anchorB: [mid[0] - world.posX[b], mid[1] - world.posY[b], mid[2] - world.posZ[b]],
96
+ }));
97
+ }
98
+ else {
99
+ links.push(world.distanceJoint({ bodyA: a, bodyB: b }));
100
+ }
101
+ }
102
+ return new Ragdoll(world, bones, links, config.balance);
103
+ }
@@ -0,0 +1,41 @@
1
+ import type { PhysicsBounds } from "./physicsWorld.js";
2
+ export interface SpatialGridConfig {
3
+ /** Horizontal extent to index; only x/z of the bounds are used (a top-down plane). */
4
+ bounds: PhysicsBounds;
5
+ /** Cell edge; tune to ≈ the query/contact radius so a hit touches ≤ 4 cells. */
6
+ cellSize: number;
7
+ /** Maximum entities the index holds; fixed at construction. */
8
+ capacity: number;
9
+ }
10
+ /**
11
+ * A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap
12
+ * same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each
13
+ * tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an
14
+ * AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false
15
+ * positives beyond the exact distance test.
16
+ */
17
+ export declare class SpatialGrid {
18
+ readonly cellSize: number;
19
+ readonly nx: number;
20
+ readonly nz: number;
21
+ readonly capacity: number;
22
+ private readonly minX;
23
+ private readonly minZ;
24
+ private readonly numCells;
25
+ private readonly cellStart;
26
+ private readonly cursor;
27
+ private readonly sorted;
28
+ private readonly cellOfBody;
29
+ private xs;
30
+ private zs;
31
+ private entityCount;
32
+ constructor(config: SpatialGridConfig);
33
+ get count(): number;
34
+ private cellX;
35
+ private cellZ;
36
+ rebuild(count: number, xs: Float32Array, zs: Float32Array): void;
37
+ /** Fill `out` with every entity within `radius` of (x,z); returns the hit count (capped at `out.length`). */
38
+ queryCircle(x: number, z: number, radius: number, out: Int32Array): number;
39
+ /** Invoke `cb(a, b)` once for each unordered pair within `maxDistance` (mutual separation/overlap). */
40
+ forEachPair(maxDistance: number, cb: (a: number, b: number) => void): void;
41
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap
3
+ * same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each
4
+ * tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an
5
+ * AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false
6
+ * positives beyond the exact distance test.
7
+ */
8
+ export class SpatialGrid {
9
+ cellSize;
10
+ nx;
11
+ nz;
12
+ capacity;
13
+ minX;
14
+ minZ;
15
+ numCells;
16
+ cellStart;
17
+ cursor;
18
+ sorted;
19
+ cellOfBody;
20
+ xs = null;
21
+ zs = null;
22
+ entityCount = 0;
23
+ constructor(config) {
24
+ this.cellSize = config.cellSize;
25
+ this.capacity = config.capacity;
26
+ this.minX = config.bounds.min[0];
27
+ this.minZ = config.bounds.min[2];
28
+ const spanX = Math.max(config.bounds.max[0] - config.bounds.min[0], config.cellSize);
29
+ const spanZ = Math.max(config.bounds.max[2] - config.bounds.min[2], config.cellSize);
30
+ this.nx = Math.max(1, Math.ceil(spanX / config.cellSize));
31
+ this.nz = Math.max(1, Math.ceil(spanZ / config.cellSize));
32
+ this.numCells = this.nx * this.nz;
33
+ this.cellStart = new Int32Array(this.numCells + 1);
34
+ this.cursor = new Int32Array(this.numCells);
35
+ this.sorted = new Int32Array(config.capacity);
36
+ this.cellOfBody = new Int32Array(config.capacity);
37
+ }
38
+ get count() {
39
+ return this.entityCount;
40
+ }
41
+ cellX(x) {
42
+ const raw = Math.floor((x - this.minX) / this.cellSize);
43
+ return raw < 0 ? 0 : raw >= this.nx ? this.nx - 1 : raw;
44
+ }
45
+ cellZ(z) {
46
+ const raw = Math.floor((z - this.minZ) / this.cellSize);
47
+ return raw < 0 ? 0 : raw >= this.nz ? this.nz - 1 : raw;
48
+ }
49
+ rebuild(count, xs, zs) {
50
+ this.entityCount = count;
51
+ this.xs = xs;
52
+ this.zs = zs;
53
+ const start = this.cellStart;
54
+ start.fill(0);
55
+ for (let i = 0; i < count; i += 1) {
56
+ const c = this.cellZ(zs[i]) * this.nx + this.cellX(xs[i]);
57
+ this.cellOfBody[i] = c;
58
+ start[c + 1] += 1;
59
+ }
60
+ for (let c = 0; c < this.numCells; c += 1) {
61
+ start[c + 1] += start[c];
62
+ this.cursor[c] = start[c];
63
+ }
64
+ for (let i = 0; i < count; i += 1) {
65
+ const c = this.cellOfBody[i];
66
+ this.sorted[this.cursor[c]++] = i;
67
+ }
68
+ }
69
+ /** Fill `out` with every entity within `radius` of (x,z); returns the hit count (capped at `out.length`). */
70
+ queryCircle(x, z, radius, out) {
71
+ const xs = this.xs;
72
+ const zs = this.zs;
73
+ if (xs === null || zs === null)
74
+ return 0;
75
+ const r2 = radius * radius;
76
+ const cxMin = this.cellX(x - radius);
77
+ const cxMax = this.cellX(x + radius);
78
+ const czMin = this.cellZ(z - radius);
79
+ const czMax = this.cellZ(z + radius);
80
+ const limit = out.length;
81
+ let n = 0;
82
+ for (let cz = czMin; cz <= czMax; cz += 1) {
83
+ const rowBase = cz * this.nx;
84
+ for (let cx = cxMin; cx <= cxMax; cx += 1) {
85
+ const cell = rowBase + cx;
86
+ const end = this.cellStart[cell + 1];
87
+ for (let s = this.cellStart[cell]; s < end; s += 1) {
88
+ const id = this.sorted[s];
89
+ const dx = xs[id] - x;
90
+ const dz = zs[id] - z;
91
+ if (dx * dx + dz * dz <= r2) {
92
+ if (n >= limit)
93
+ return n;
94
+ out[n++] = id;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ return n;
100
+ }
101
+ /** Invoke `cb(a, b)` once for each unordered pair within `maxDistance` (mutual separation/overlap). */
102
+ forEachPair(maxDistance, cb) {
103
+ const xs = this.xs;
104
+ const zs = this.zs;
105
+ if (xs === null || zs === null)
106
+ return;
107
+ const d2 = maxDistance * maxDistance;
108
+ const nx = this.nx;
109
+ const nz = this.nz;
110
+ for (let i = 0; i < this.entityCount; i += 1) {
111
+ const c = this.cellOfBody[i];
112
+ const cz = (c / nx) | 0;
113
+ const cx = c - cz * nx;
114
+ const x = xs[i];
115
+ const z = zs[i];
116
+ const z0 = cz > 0 ? cz - 1 : cz;
117
+ const z1 = cz < nz - 1 ? cz + 1 : cz;
118
+ const x0 = cx > 0 ? cx - 1 : cx;
119
+ const x1 = cx < nx - 1 ? cx + 1 : cx;
120
+ for (let gz = z0; gz <= z1; gz += 1) {
121
+ const rowBase = gz * nx;
122
+ for (let gx = x0; gx <= x1; gx += 1) {
123
+ const cell = rowBase + gx;
124
+ const end = this.cellStart[cell + 1];
125
+ for (let s = this.cellStart[cell]; s < end; s += 1) {
126
+ const j = this.sorted[s];
127
+ if (j <= i)
128
+ continue;
129
+ const dx = xs[j] - x;
130
+ const dz = zs[j] - z;
131
+ if (dx * dx + dz * dz <= d2)
132
+ cb(i, j);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }