@jgengine/core 0.8.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 (228) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.js +1 -1
  11. package/dist/ai/threat.d.ts +2 -0
  12. package/dist/ai/threat.js +34 -0
  13. package/dist/cartridge/runtime.d.ts +15 -0
  14. package/dist/cartridge/runtime.js +556 -0
  15. package/dist/cartridge/spec.d.ts +188 -0
  16. package/dist/cartridge/spec.js +19 -0
  17. package/dist/cartridge/validate.d.ts +2 -0
  18. package/dist/cartridge/validate.js +78 -0
  19. package/dist/combat/abilityKit.d.ts +17 -2
  20. package/dist/combat/abilityKit.js +62 -13
  21. package/dist/combat/castRunner.d.ts +40 -0
  22. package/dist/combat/castRunner.js +56 -0
  23. package/dist/combat/comboPoints.d.ts +14 -0
  24. package/dist/combat/comboPoints.js +52 -0
  25. package/dist/combat/defensiveWindow.d.ts +1 -1
  26. package/dist/combat/defensiveWindow.js +5 -3
  27. package/dist/combat/projectiles.d.ts +33 -12
  28. package/dist/combat/projectiles.js +201 -129
  29. package/dist/combat/resistance.d.ts +16 -4
  30. package/dist/combat/resistance.js +43 -7
  31. package/dist/combat/resourcePool.d.ts +20 -0
  32. package/dist/combat/resourcePool.js +55 -0
  33. package/dist/combat/shotOrigin.d.ts +40 -0
  34. package/dist/combat/shotOrigin.js +57 -0
  35. package/dist/combat/telegraph.d.ts +25 -0
  36. package/dist/combat/telegraph.js +47 -0
  37. package/dist/crafting/recipe.js +4 -1
  38. package/dist/devtools/devtools.d.ts +72 -24
  39. package/dist/devtools/devtools.js +449 -101
  40. package/dist/devtools/transformTunables.js +3 -3
  41. package/dist/devtools/tunableSchema.d.ts +140 -0
  42. package/dist/devtools/tunableSchema.js +476 -0
  43. package/dist/faction/factions.d.ts +33 -0
  44. package/dist/faction/factions.js +76 -0
  45. package/dist/faction/reputation.d.ts +32 -0
  46. package/dist/faction/reputation.js +82 -0
  47. package/dist/game/chat.d.ts +2 -0
  48. package/dist/game/chat.js +9 -1
  49. package/dist/game/chatFilter.d.ts +15 -0
  50. package/dist/game/chatFilter.js +55 -0
  51. package/dist/game/controlGate.d.ts +4 -0
  52. package/dist/game/controlGate.js +7 -0
  53. package/dist/game/defineGame.d.ts +2 -0
  54. package/dist/game/events.d.ts +10 -0
  55. package/dist/game/gamePhase.d.ts +20 -0
  56. package/dist/game/gamePhase.js +21 -0
  57. package/dist/game/lootTable.d.ts +10 -1
  58. package/dist/game/lootTable.js +27 -7
  59. package/dist/game/playableGame.d.ts +50 -1
  60. package/dist/game/playableGame.js +1 -1
  61. package/dist/game/quest.d.ts +7 -3
  62. package/dist/game/quest.js +17 -8
  63. package/dist/game/race.d.ts +39 -1
  64. package/dist/game/race.js +96 -11
  65. package/dist/game/recordBook.d.ts +29 -0
  66. package/dist/game/recordBook.js +86 -0
  67. package/dist/game/talents.d.ts +47 -0
  68. package/dist/game/talents.js +140 -0
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +1 -0
  71. package/dist/input/axisInput.d.ts +26 -3
  72. package/dist/input/axisInput.js +44 -4
  73. package/dist/input/bindingOverrides.d.ts +25 -0
  74. package/dist/input/bindingOverrides.js +95 -0
  75. package/dist/input/pointerAxis.d.ts +30 -0
  76. package/dist/input/pointerAxis.js +31 -0
  77. package/dist/input/touchScheme.d.ts +6 -0
  78. package/dist/input/touchScheme.js +45 -7
  79. package/dist/meta/changelog.d.ts +1 -1
  80. package/dist/meta/changelog.js +23 -1
  81. package/dist/movement/dash.d.ts +3 -0
  82. package/dist/movement/dash.js +19 -5
  83. package/dist/movement/glideModel.d.ts +44 -0
  84. package/dist/movement/glideModel.js +54 -0
  85. package/dist/movement/grappleSwing.d.ts +34 -0
  86. package/dist/movement/grappleSwing.js +56 -0
  87. package/dist/movement/leaderTrail.d.ts +29 -0
  88. package/dist/movement/leaderTrail.js +67 -0
  89. package/dist/movement/steering.d.ts +27 -0
  90. package/dist/movement/steering.js +40 -0
  91. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  92. package/dist/multiplayer/feedWriteGate.js +12 -0
  93. package/dist/multiplayer/presenceModel.d.ts +14 -2
  94. package/dist/multiplayer/presenceModel.js +15 -3
  95. package/dist/nav/corridors.d.ts +30 -0
  96. package/dist/nav/corridors.js +69 -0
  97. package/dist/nav/navGrid.js +30 -4
  98. package/dist/nav/railGraph.d.ts +66 -0
  99. package/dist/nav/railGraph.js +129 -0
  100. package/dist/nav/timetable.d.ts +41 -0
  101. package/dist/nav/timetable.js +94 -0
  102. package/dist/physics/buoyancy.d.ts +6 -0
  103. package/dist/physics/buoyancy.js +14 -0
  104. package/dist/physics/flowTube.d.ts +32 -0
  105. package/dist/physics/flowTube.js +64 -0
  106. package/dist/physics/forceVolume.d.ts +27 -0
  107. package/dist/physics/forceVolume.js +38 -0
  108. package/dist/physics/kinematicVehicle.d.ts +52 -0
  109. package/dist/physics/kinematicVehicle.js +81 -0
  110. package/dist/physics/physicsWorld.d.ts +13 -6
  111. package/dist/physics/physicsWorld.js +169 -40
  112. package/dist/physics/ragdoll.js +1 -1
  113. package/dist/physics/vehicleBody.js +2 -1
  114. package/dist/random/seedLink.d.ts +5 -0
  115. package/dist/random/seedLink.js +35 -0
  116. package/dist/runtime/cameraDirector.d.ts +6 -1
  117. package/dist/runtime/cameraDirector.js +6 -0
  118. package/dist/runtime/commandRunner.d.ts +2 -2
  119. package/dist/runtime/commandRunner.js +2 -2
  120. package/dist/runtime/gameContext.d.ts +21 -0
  121. package/dist/runtime/gameContext.js +184 -9
  122. package/dist/runtime/gameRuntime.d.ts +5 -1
  123. package/dist/runtime/gameRuntime.js +13 -12
  124. package/dist/runtime/hostPersistence.d.ts +9 -1
  125. package/dist/runtime/hostPersistence.js +23 -8
  126. package/dist/runtime/inputSnapshot.d.ts +5 -0
  127. package/dist/runtime/inputSnapshot.js +5 -0
  128. package/dist/runtime/motionIntents.d.ts +14 -6
  129. package/dist/runtime/motionIntents.js +20 -2
  130. package/dist/runtime/snapshot.d.ts +1 -0
  131. package/dist/runtime/snapshot.js +1 -1
  132. package/dist/runtime/visibility.d.ts +74 -0
  133. package/dist/runtime/visibility.js +127 -0
  134. package/dist/scene/colliders.d.ts +44 -0
  135. package/dist/scene/colliders.js +77 -0
  136. package/dist/scene/entityStore.d.ts +22 -5
  137. package/dist/scene/entityStore.js +59 -12
  138. package/dist/scene/objectQuery.d.ts +13 -2
  139. package/dist/scene/objectQuery.js +34 -2
  140. package/dist/scene/objectStore.d.ts +2 -0
  141. package/dist/scene/objectStore.js +31 -12
  142. package/dist/scene/sceneRaycast.d.ts +80 -0
  143. package/dist/scene/sceneRaycast.js +284 -0
  144. package/dist/scene/spatial.d.ts +11 -16
  145. package/dist/scene/spatial.js +99 -38
  146. package/dist/sensor/recordingBuffer.d.ts +6 -0
  147. package/dist/sensor/recordingBuffer.js +7 -0
  148. package/dist/sensor/replayLoop.d.ts +39 -0
  149. package/dist/sensor/replayLoop.js +64 -0
  150. package/dist/sensor/visionCone.d.ts +31 -0
  151. package/dist/sensor/visionCone.js +72 -0
  152. package/dist/session/extraction.d.ts +2 -2
  153. package/dist/session/extraction.js +13 -2
  154. package/dist/settings/settingsModel.d.ts +97 -0
  155. package/dist/settings/settingsModel.js +100 -0
  156. package/dist/stats/statModifiers.d.ts +5 -2
  157. package/dist/stats/statModifiers.js +32 -14
  158. package/dist/store/observableKeyedStore.js +8 -2
  159. package/dist/survival/decayMeter.d.ts +1 -1
  160. package/dist/survival/decayMeter.js +0 -1
  161. package/dist/time/beatClock.d.ts +18 -0
  162. package/dist/time/beatClock.js +24 -0
  163. package/dist/time/idleProgress.d.ts +29 -0
  164. package/dist/time/idleProgress.js +31 -0
  165. package/dist/time/serverTick.d.ts +7 -1
  166. package/dist/time/serverTick.js +27 -4
  167. package/dist/time/stateSchedule.d.ts +57 -0
  168. package/dist/time/stateSchedule.js +121 -0
  169. package/dist/turn/commit.d.ts +2 -2
  170. package/dist/turn/commit.js +2 -2
  171. package/dist/ui/hudLayout.d.ts +58 -0
  172. package/dist/ui/hudLayout.js +207 -0
  173. package/dist/ui/hudScale.d.ts +44 -0
  174. package/dist/ui/hudScale.js +55 -0
  175. package/dist/visibility/assetStreaming.d.ts +66 -0
  176. package/dist/visibility/assetStreaming.js +201 -0
  177. package/dist/visibility/bounds.d.ts +66 -0
  178. package/dist/visibility/bounds.js +113 -0
  179. package/dist/visibility/camera.d.ts +21 -0
  180. package/dist/visibility/camera.js +3 -0
  181. package/dist/visibility/config.d.ts +21 -0
  182. package/dist/visibility/config.js +1 -0
  183. package/dist/visibility/diagnostics.d.ts +26 -0
  184. package/dist/visibility/diagnostics.js +36 -0
  185. package/dist/visibility/distance.d.ts +11 -0
  186. package/dist/visibility/distance.js +30 -0
  187. package/dist/visibility/frustum.d.ts +47 -0
  188. package/dist/visibility/frustum.js +186 -0
  189. package/dist/visibility/occlusion.d.ts +36 -0
  190. package/dist/visibility/occlusion.js +45 -0
  191. package/dist/visibility/settings.d.ts +75 -0
  192. package/dist/visibility/settings.js +49 -0
  193. package/dist/visibility/simulationCulling.d.ts +34 -0
  194. package/dist/visibility/simulationCulling.js +36 -0
  195. package/dist/visibility/spatialIndex.d.ts +45 -0
  196. package/dist/visibility/spatialIndex.js +198 -0
  197. package/dist/visibility/visibilitySystem.d.ts +90 -0
  198. package/dist/visibility/visibilitySystem.js +257 -0
  199. package/dist/world/buildings.d.ts +6 -0
  200. package/dist/world/buildings.js +152 -0
  201. package/dist/world/carve.d.ts +7 -2
  202. package/dist/world/carve.js +17 -7
  203. package/dist/world/cellStates.d.ts +45 -0
  204. package/dist/world/cellStates.js +103 -0
  205. package/dist/world/environmentSummary.d.ts +10 -2
  206. package/dist/world/environmentSummary.js +11 -2
  207. package/dist/world/features.d.ts +56 -8
  208. package/dist/world/features.js +24 -3
  209. package/dist/world/gridInstances.d.ts +7 -8
  210. package/dist/world/gridInstances.js +145 -7
  211. package/dist/world/lod.d.ts +17 -0
  212. package/dist/world/lod.js +66 -0
  213. package/dist/world/mapLayers.d.ts +60 -0
  214. package/dist/world/mapLayers.js +44 -0
  215. package/dist/world/massing.d.ts +45 -0
  216. package/dist/world/massing.js +581 -0
  217. package/dist/world/minimap.d.ts +13 -1
  218. package/dist/world/minimap.js +21 -2
  219. package/dist/world/terrain.d.ts +42 -3
  220. package/dist/world/terrain.js +197 -16
  221. package/dist/world/water.d.ts +7 -0
  222. package/dist/world/water.js +19 -4
  223. package/dist/world/weather.d.ts +3 -18
  224. package/dist/world/weather.js +4 -17
  225. package/dist/world/windZones.d.ts +47 -0
  226. package/dist/world/windZones.js +59 -0
  227. package/llms.txt +2585 -1704
  228. package/package.json +12 -2
@@ -1,5 +1,9 @@
1
1
  export const SHAPE_BOX = 0;
2
2
  export const SHAPE_SPHERE = 1;
3
+ export const JOINT_FIXED = 0;
4
+ export const JOINT_HINGE = 1;
5
+ export const JOINT_DISTANCE = 2;
6
+ export const JOINT_SPRING = 3;
3
7
  const FLAG_SLEEPING = 1;
4
8
  const FLAG_STATIC = 2;
5
9
  const FLAG_DEAD = 4;
@@ -65,7 +69,7 @@ export class PhysicsWorld {
65
69
  awakeList;
66
70
  awakeSlot;
67
71
  awakeCount = 0;
68
- jSpring;
72
+ jKind;
69
73
  jActive;
70
74
  jBodyA;
71
75
  jBodyB;
@@ -75,6 +79,9 @@ export class PhysicsWorld {
75
79
  jAnchorBX;
76
80
  jAnchorBY;
77
81
  jAnchorBZ;
82
+ jAxisX;
83
+ jAxisY;
84
+ jAxisZ;
78
85
  jRest;
79
86
  jStiffness;
80
87
  jDamping;
@@ -168,7 +175,7 @@ export class PhysicsWorld {
168
175
  this.awakeSlot = new Int32Array(cap).fill(-1);
169
176
  this.bodyFree = new Int32Array(cap);
170
177
  const jc = this.jointCapacity;
171
- this.jSpring = new Uint8Array(jc);
178
+ this.jKind = new Uint8Array(jc);
172
179
  this.jActive = new Uint8Array(jc);
173
180
  this.jBodyA = new Int32Array(jc);
174
181
  this.jBodyB = new Int32Array(jc);
@@ -178,6 +185,9 @@ export class PhysicsWorld {
178
185
  this.jAnchorBX = new Float32Array(jc);
179
186
  this.jAnchorBY = new Float32Array(jc);
180
187
  this.jAnchorBZ = new Float32Array(jc);
188
+ this.jAxisX = new Float32Array(jc);
189
+ this.jAxisY = new Float32Array(jc);
190
+ this.jAxisZ = new Float32Array(jc);
181
191
  this.jRest = new Float32Array(jc);
182
192
  this.jStiffness = new Float32Array(jc);
183
193
  this.jDamping = new Float32Array(jc);
@@ -224,18 +234,25 @@ export class PhysicsWorld {
224
234
  this.collisionMinSpeed = minApproachSpeed;
225
235
  }
226
236
  hingeJoint(options) {
227
- return this.addJoint(options, false, 0);
237
+ const raw = options.axis ?? [0, 1, 0];
238
+ const len = Math.hypot(raw[0], raw[1], raw[2]);
239
+ if (!(len > 1e-9))
240
+ throw new Error("PhysicsWorld: hingeJoint axis must be non-zero");
241
+ return this.addJoint(options, JOINT_HINGE, 0, [raw[0] / len, raw[1] / len, raw[2] / len]);
228
242
  }
229
243
  fixedJoint(options) {
230
- return this.addJoint(options, false, 0);
244
+ return this.addJoint(options, JOINT_FIXED, 0, null);
231
245
  }
232
246
  distanceJoint(options) {
233
- return this.addJoint(options, false, null);
247
+ return this.addJoint(options, JOINT_DISTANCE, null, null);
234
248
  }
235
249
  springJoint(options) {
236
- return this.addJoint(options, true, null);
250
+ return this.addJoint(options, JOINT_SPRING, null, null);
237
251
  }
238
- addJoint(options, spring, restOverride) {
252
+ addJoint(options, kind, restOverride, axis) {
253
+ if (kind !== JOINT_FIXED && kind !== JOINT_HINGE && kind !== JOINT_DISTANCE && kind !== JOINT_SPRING) {
254
+ throw new Error(`PhysicsWorld: joint kind ${kind} is not implemented`);
255
+ }
239
256
  let id;
240
257
  if (this.jFreeCount > 0) {
241
258
  id = this.jFree[--this.jFreeCount];
@@ -256,7 +273,10 @@ export class PhysicsWorld {
256
273
  this.jAnchorBX[id] = aB?.[0] ?? 0;
257
274
  this.jAnchorBY[id] = aB?.[1] ?? 0;
258
275
  this.jAnchorBZ[id] = aB?.[2] ?? 0;
259
- this.jSpring[id] = spring ? 1 : 0;
276
+ this.jKind[id] = kind;
277
+ this.jAxisX[id] = axis?.[0] ?? 0;
278
+ this.jAxisY[id] = axis?.[1] ?? 0;
279
+ this.jAxisZ[id] = axis?.[2] ?? 0;
260
280
  this.jStiffness[id] = options.stiffness ?? 40;
261
281
  this.jDamping[id] = options.damping ?? 6;
262
282
  this.jMaxImpulse[id] = options.maxImpulse ?? Number.POSITIVE_INFINITY;
@@ -587,28 +607,56 @@ export class PhysicsWorld {
587
607
  const n = this.bodyHigh;
588
608
  const start = this.cellStart;
589
609
  start.fill(0);
590
- // Sleeping bodies never move, so their cell index is cached from addBody / their last awake
591
- // substep — only awake bodies pay the cell recomputation each substep.
592
610
  const awakeCount = this.awakeCount;
593
611
  for (let a = 0; a < awakeCount; a += 1) {
594
612
  const i = this.awakeList[a];
595
613
  this.bodyCell[i] = this.cellOf(i);
596
614
  }
597
- // Dead slots (tombstoned by removeBody) sit inside [0, bodyHigh) but never enter the grid.
615
+ const nx = this.nx;
616
+ const ny = this.ny;
598
617
  for (let i = 0; i < n; i += 1) {
599
618
  if ((this.flags[i] & FLAG_DEAD) !== 0)
600
619
  continue;
601
- start[this.bodyCell[i] + 1] += 1;
620
+ const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
621
+ const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
622
+ const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
623
+ const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
624
+ const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
625
+ const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
626
+ for (let z = z0; z <= z1; z += 1) {
627
+ for (let y = y0; y <= y1; y += 1) {
628
+ const rowBase = (z * ny + y) * nx;
629
+ for (let x = x0; x <= x1; x += 1)
630
+ start[rowBase + x + 1] += 1;
631
+ }
632
+ }
602
633
  }
603
634
  for (let c = 0; c < this.numCells; c += 1) {
604
635
  start[c + 1] += start[c];
605
636
  this.cursor[c] = start[c];
606
637
  }
638
+ const total = start[this.numCells];
639
+ if (total > this.sorted.length) {
640
+ this.sorted = new Int32Array(Math.max(total, this.sorted.length * 2));
641
+ }
607
642
  for (let i = 0; i < n; i += 1) {
608
643
  if ((this.flags[i] & FLAG_DEAD) !== 0)
609
644
  continue;
610
- const c = this.bodyCell[i];
611
- this.sorted[this.cursor[c]++] = i;
645
+ const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
646
+ const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
647
+ const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
648
+ const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
649
+ const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
650
+ const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
651
+ for (let z = z0; z <= z1; z += 1) {
652
+ for (let y = y0; y <= y1; y += 1) {
653
+ const rowBase = (z * ny + y) * nx;
654
+ for (let x = x0; x <= x1; x += 1) {
655
+ const c = rowBase + x;
656
+ this.sorted[this.cursor[c]++] = i;
657
+ }
658
+ }
659
+ }
612
660
  }
613
661
  }
614
662
  cellOf(i) {
@@ -620,26 +668,18 @@ export class PhysicsWorld {
620
668
  solve(restitutionPass, countPass) {
621
669
  const nx = this.nx;
622
670
  const ny = this.ny;
623
- const nz = this.nz;
624
- const nxny = nx * ny;
625
- // A contact may wake a sleeping body mid-pass, appending it to the awake list; re-read the
626
- // count each iteration so it is solved this substep too.
627
671
  for (let a = 0; a < this.awakeCount; a += 1) {
628
672
  const i = this.awakeList[a];
629
- const c = this.bodyCell[i];
630
- const cz = (c / nxny) | 0;
631
- const cy = ((c - cz * nxny) / nx) | 0;
632
- const cx = c - cz * nxny - cy * nx;
633
- const z0 = cz > 0 ? cz - 1 : cz;
634
- const z1 = cz < nz - 1 ? cz + 1 : cz;
635
- const y0 = cy > 0 ? cy - 1 : cy;
636
- const y1 = cy < ny - 1 ? cy + 1 : cy;
637
- const x0 = cx > 0 ? cx - 1 : cx;
638
- const x1 = cx < nx - 1 ? cx + 1 : cx;
639
- for (let z = z0; z <= z1; z += 1) {
640
- for (let y = y0; y <= y1; y += 1) {
673
+ const ix0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
674
+ const ix1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
675
+ const iy0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
676
+ const iy1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
677
+ const iz0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
678
+ const iz1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
679
+ for (let z = iz0; z <= iz1; z += 1) {
680
+ for (let y = iy0; y <= iy1; y += 1) {
641
681
  const rowBase = (z * ny + y) * nx;
642
- for (let x = x0; x <= x1; x += 1) {
682
+ for (let x = ix0; x <= ix1; x += 1) {
643
683
  const cell = rowBase + x;
644
684
  const end = this.cellStart[cell + 1];
645
685
  for (let s = this.cellStart[cell]; s < end; s += 1) {
@@ -649,6 +689,14 @@ export class PhysicsWorld {
649
689
  const jSleeping = (this.flags[j] & FLAG_SLEEPING) !== 0;
650
690
  if (!jSleeping && j < i)
651
691
  continue;
692
+ const jx0 = cellCoord(this.posX[j] - this.halfX[j], this.bounds.min[0], this.cellSize, this.nx);
693
+ const jy0 = cellCoord(this.posY[j] - this.halfY[j], this.bounds.min[1], this.cellSize, this.ny);
694
+ const jz0 = cellCoord(this.posZ[j] - this.halfZ[j], this.bounds.min[2], this.cellSize, this.nz);
695
+ const mx0 = ix0 > jx0 ? ix0 : jx0;
696
+ const my0 = iy0 > jy0 ? iy0 : jy0;
697
+ const mz0 = iz0 > jz0 ? iz0 : jz0;
698
+ if (x !== mx0 || y !== my0 || z !== mz0)
699
+ continue;
652
700
  this.resolve(i, j, restitutionPass, countPass);
653
701
  }
654
702
  }
@@ -819,6 +867,7 @@ export class PhysicsWorld {
819
867
  for (let id = 0; id < this.jointHigh; id += 1) {
820
868
  if (this.jActive[id] !== 1)
821
869
  continue;
870
+ const kind = this.jKind[id];
822
871
  const bA = this.jBodyA[id];
823
872
  const bB = this.jBodyB[id];
824
873
  const ax = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
@@ -827,9 +876,91 @@ export class PhysicsWorld {
827
876
  const bx = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
828
877
  const by = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
829
878
  const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
830
- const dx = bx - ax;
831
- const dy = by - ay;
832
- const dz = bz - az;
879
+ let dx = bx - ax;
880
+ let dy = by - ay;
881
+ let dz = bz - az;
882
+ const imA = bA >= 0 ? this.invMass[bA] : 0;
883
+ const imB = bB >= 0 ? this.invMass[bB] : 0;
884
+ const imSum = imA + imB;
885
+ if (imSum === 0)
886
+ continue;
887
+ if (kind === JOINT_HINGE) {
888
+ const ux = this.jAxisX[id];
889
+ const uy = this.jAxisY[id];
890
+ const uz = this.jAxisZ[id];
891
+ const along = dx * ux + dy * uy + dz * uz;
892
+ dx -= along * ux;
893
+ dy -= along * uy;
894
+ dz -= along * uz;
895
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
896
+ const vax = bA >= 0 ? this.velX[bA] : 0;
897
+ const vay = bA >= 0 ? this.velY[bA] : 0;
898
+ const vaz = bA >= 0 ? this.velZ[bA] : 0;
899
+ const vbx = bB >= 0 ? this.velX[bB] : 0;
900
+ const vby = bB >= 0 ? this.velY[bB] : 0;
901
+ const vbz = bB >= 0 ? this.velZ[bB] : 0;
902
+ let rvx = vbx - vax;
903
+ let rvy = vby - vay;
904
+ let rvz = vbz - vaz;
905
+ const rvAlong = rvx * ux + rvy * uy + rvz * uz;
906
+ rvx -= rvAlong * ux;
907
+ rvy -= rvAlong * uy;
908
+ rvz -= rvAlong * uz;
909
+ if (dist > 1e-9) {
910
+ const nX = dx / dist;
911
+ const nY = dy / dist;
912
+ const nZ = dz / dist;
913
+ if (dist > slop) {
914
+ if (bA >= 0)
915
+ this.wake(bA);
916
+ if (bB >= 0)
917
+ this.wake(bB);
918
+ }
919
+ const rvn = rvx * nX + rvy * nY + rvz * nZ;
920
+ const jn = -rvn / imSum;
921
+ if (bA >= 0) {
922
+ this.velX[bA] -= jn * imA * nX;
923
+ this.velY[bA] -= jn * imA * nY;
924
+ this.velZ[bA] -= jn * imA * nZ;
925
+ }
926
+ if (bB >= 0) {
927
+ this.velX[bB] += jn * imB * nX;
928
+ this.velY[bB] += jn * imB * nY;
929
+ this.velZ[bB] += jn * imB * nZ;
930
+ }
931
+ const corr = (dist * beta) / imSum;
932
+ if (bA >= 0) {
933
+ this.posX[bA] += corr * imA * nX;
934
+ this.posY[bA] += corr * imA * nY;
935
+ this.posZ[bA] += corr * imA * nZ;
936
+ }
937
+ if (bB >= 0) {
938
+ this.posX[bB] -= corr * imB * nX;
939
+ this.posY[bB] -= corr * imB * nY;
940
+ this.posZ[bB] -= corr * imB * nZ;
941
+ }
942
+ }
943
+ else {
944
+ const rvl = Math.sqrt(rvx * rvx + rvy * rvy + rvz * rvz);
945
+ if (rvl > 1e-9) {
946
+ const nX = rvx / rvl;
947
+ const nY = rvy / rvl;
948
+ const nZ = rvz / rvl;
949
+ const jn = -rvl / imSum;
950
+ if (bA >= 0) {
951
+ this.velX[bA] -= jn * imA * nX;
952
+ this.velY[bA] -= jn * imA * nY;
953
+ this.velZ[bA] -= jn * imA * nZ;
954
+ }
955
+ if (bB >= 0) {
956
+ this.velX[bB] += jn * imB * nX;
957
+ this.velY[bB] += jn * imB * nY;
958
+ this.velZ[bB] += jn * imB * nZ;
959
+ }
960
+ }
961
+ }
962
+ continue;
963
+ }
833
964
  const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
834
965
  if (dist < 1e-9)
835
966
  continue;
@@ -837,11 +968,6 @@ export class PhysicsWorld {
837
968
  const nY = dy / dist;
838
969
  const nZ = dz / dist;
839
970
  const c = dist - this.jRest[id];
840
- const imA = bA >= 0 ? this.invMass[bA] : 0;
841
- const imB = bB >= 0 ? this.invMass[bB] : 0;
842
- const imSum = imA + imB;
843
- if (imSum === 0)
844
- continue;
845
971
  const vax = bA >= 0 ? this.velX[bA] : 0;
846
972
  const vay = bA >= 0 ? this.velY[bA] : 0;
847
973
  const vaz = bA >= 0 ? this.velZ[bA] : 0;
@@ -849,7 +975,7 @@ export class PhysicsWorld {
849
975
  const vby = bB >= 0 ? this.velY[bB] : 0;
850
976
  const vbz = bB >= 0 ? this.velZ[bB] : 0;
851
977
  const rvn = (vbx - vax) * nX + (vby - vay) * nY + (vbz - vaz) * nZ;
852
- if (this.jSpring[id] === 1) {
978
+ if (kind === JOINT_SPRING) {
853
979
  if (Math.abs(c) > slop) {
854
980
  if (bA >= 0)
855
981
  this.wake(bA);
@@ -874,6 +1000,9 @@ export class PhysicsWorld {
874
1000
  }
875
1001
  continue;
876
1002
  }
1003
+ if (kind !== JOINT_FIXED && kind !== JOINT_DISTANCE) {
1004
+ throw new Error(`PhysicsWorld: joint kind ${kind} is not implemented`);
1005
+ }
877
1006
  if (Math.abs(c) > slop) {
878
1007
  if (bA >= 0)
879
1008
  this.wake(bA);
@@ -88,7 +88,7 @@ export function createRagdoll(world, config) {
88
88
  (world.posY[a] + world.posY[b]) / 2,
89
89
  (world.posZ[a] + world.posZ[b]) / 2,
90
90
  ];
91
- links.push(world.hingeJoint({
91
+ links.push(world.fixedJoint({
92
92
  bodyA: a,
93
93
  bodyB: b,
94
94
  anchorA: [mid[0] - world.posX[a], mid[1] - world.posY[a], mid[2] - world.posZ[a]],
@@ -1,3 +1,4 @@
1
+ import { steerYaw } from "../movement/steering.js";
1
2
  /**
2
3
  * Piecewise-linear tire-grip curve: normalized lateral slip → available grip (0..1). Grip peaks near
3
4
  * the breakaway slip then falls off as the tire slides — the shape that separates a planted corner
@@ -166,7 +167,7 @@ export class VehicleBody {
166
167
  if (grounded) {
167
168
  const steerScale = Math.min(1, Math.abs(speed) / this.turnSpeedRef);
168
169
  const dir = speed >= 0 ? 1 : -1;
169
- this.heading += input.steer * this.turnRate * steerScale * dir * dt;
170
+ this.heading = steerYaw(this.heading, input.steer * steerScale * dir, this.turnRate, dt);
170
171
  }
171
172
  const [fx, fz] = this.forward;
172
173
  if (grounded) {
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_SEED_PARAM = "seed";
2
+ export declare function withSeedParam(url: string, seed: string | number, param?: string): string;
3
+ export declare function seedFromSearch(search: string, param?: string): string | null;
4
+ export declare function seedFromUrl(url: string, param?: string): string | null;
5
+ export declare function dailySeed(nowMs: number, salt?: string): string;
@@ -0,0 +1,35 @@
1
+ export const DEFAULT_SEED_PARAM = "seed";
2
+ function splitUrl(url) {
3
+ const hashIndex = url.indexOf("#");
4
+ const hash = hashIndex === -1 ? "" : url.slice(hashIndex + 1);
5
+ const rest = hashIndex === -1 ? url : url.slice(0, hashIndex);
6
+ const queryIndex = rest.indexOf("?");
7
+ const base = queryIndex === -1 ? rest : rest.slice(0, queryIndex);
8
+ const query = queryIndex === -1 ? "" : rest.slice(queryIndex + 1);
9
+ return { base, query, hash };
10
+ }
11
+ export function withSeedParam(url, seed, param = DEFAULT_SEED_PARAM) {
12
+ const { base, query, hash } = splitUrl(url);
13
+ const params = new URLSearchParams(query);
14
+ params.set(param, typeof seed === "number" ? seed.toString() : seed);
15
+ const queryString = params.toString();
16
+ return `${base}${queryString ? `?${queryString}` : ""}${hash ? `#${hash}` : ""}`;
17
+ }
18
+ export function seedFromSearch(search, param = DEFAULT_SEED_PARAM) {
19
+ const normalized = search.startsWith("?") ? search.slice(1) : search;
20
+ const params = new URLSearchParams(normalized);
21
+ const value = params.get(param);
22
+ return value === null || value === "" ? null : value;
23
+ }
24
+ export function seedFromUrl(url, param = DEFAULT_SEED_PARAM) {
25
+ const { query } = splitUrl(url);
26
+ return seedFromSearch(query, param);
27
+ }
28
+ export function dailySeed(nowMs, salt) {
29
+ const date = new Date(nowMs);
30
+ const year = date.getUTCFullYear().toString().padStart(4, "0");
31
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
32
+ const day = date.getUTCDate().toString().padStart(2, "0");
33
+ const base = `${year}-${month}-${day}`;
34
+ return salt !== undefined && salt.length > 0 ? `${base}:${salt}` : base;
35
+ }
@@ -1,10 +1,15 @@
1
- import type { CinematicCameraConfig } from "../game/playableGame.js";
1
+ import type { ChaseCameraConfig, CinematicCameraConfig } from "../game/playableGame.js";
2
+ /** Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11). */
3
+ export type ChaseCameraTuning = Partial<Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank">>;
2
4
  export interface CameraDirector {
3
5
  follow(entityId: string | null): void;
4
6
  /** `undefined` means no runtime override — the shell falls back to the static `playable.camera.followEntityId`. `null` means explicitly follow nothing. */
5
7
  followedEntityId(): string | null | undefined;
6
8
  setCinematic(config: CinematicCameraConfig | null): void;
7
9
  cinematic(): CinematicCameraConfig | null;
10
+ /** Overlay a runtime patch on the chase rig's config — a boss-intro pull-back, drift zoom-out; `null` restores the static config. */
11
+ setChaseTuning(tuning: ChaseCameraTuning | null): void;
12
+ chaseTuning(): ChaseCameraTuning | null;
8
13
  subscribe(listener: () => void): () => void;
9
14
  }
10
15
  export declare function createCameraDirector(): CameraDirector;
@@ -2,6 +2,7 @@ export function createCameraDirector() {
2
2
  const listeners = new Set();
3
3
  let followEntity = undefined;
4
4
  let cinematicConfig = null;
5
+ let chaseTuningPatch = null;
5
6
  function notify() {
6
7
  for (const listener of listeners)
7
8
  listener();
@@ -17,6 +18,11 @@ export function createCameraDirector() {
17
18
  notify();
18
19
  },
19
20
  cinematic: () => cinematicConfig,
21
+ setChaseTuning(tuning) {
22
+ chaseTuningPatch = tuning;
23
+ notify();
24
+ },
25
+ chaseTuning: () => chaseTuningPatch,
20
26
  subscribe(listener) {
21
27
  listeners.add(listener);
22
28
  return () => listeners.delete(listener);
@@ -2,8 +2,8 @@ export type CommandValidationError = {
2
2
  reason: string;
3
3
  };
4
4
  export type CommandDef<TInput = unknown> = {
5
- validate: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput) => CommandValidationError | null;
6
- apply: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput) => import("./snapshot.js").GameRuntimeSnapshot;
5
+ validate: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string) => CommandValidationError | null;
6
+ apply: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string) => import("./snapshot.js").GameRuntimeSnapshot;
7
7
  };
8
8
  export type RunCommandResult = {
9
9
  ok: true;
@@ -3,11 +3,11 @@ export function runCommand(snapshot, commands, commandName, input, actorUserId)
3
3
  if (!command) {
4
4
  return { ok: false, reason: `Unknown command: ${commandName}` };
5
5
  }
6
- const validationError = command.validate(snapshot, input);
6
+ const validationError = command.validate(snapshot, input, actorUserId);
7
7
  if (validationError) {
8
8
  return { ok: false, reason: validationError.reason };
9
9
  }
10
- const next = command.apply(snapshot, input);
10
+ const next = command.apply(snapshot, input, actorUserId);
11
11
  return {
12
12
  ok: true,
13
13
  snapshot: {
@@ -30,16 +30,19 @@ import { type PaintLayer } from "../scene/paintLayer.js";
30
30
  import { type EntityStatsApi, type StatCatalog } from "../scene/entityStats.js";
31
31
  import type { EntityPose, EntityPosition, EntityStore, SceneEntity, SpawnOptions, SpawnPose } from "../scene/entityStore.js";
32
32
  import { type Forms } from "../scene/form.js";
33
+ import type { EntityColliderSet } from "../scene/colliders.js";
33
34
  import { type ObjectRaycastHit, type ObjectRaycastInput } from "../scene/objectQuery.js";
34
35
  import { type ObjectStore } from "../scene/objectStore.js";
35
36
  import { type Roster } from "../scene/roster.js";
36
37
  import { type Possession } from "../scene/possession.js";
38
+ import { type SceneRaycastHit, type SceneRaycastInput } from "../scene/sceneRaycast.js";
37
39
  import { type SpatialApi } from "../scene/spatial.js";
38
40
  import { type CycleTargetOptions } from "../scene/targeting.js";
39
41
  import { type Stats } from "../stats/statModifiers.js";
40
42
  import { type ObservableKeyedStore } from "../store/observableKeyedStore.js";
41
43
  import { type SimClock } from "../time/simClock.js";
42
44
  import { type TurnLoop, type TurnLoopConfig } from "../turn/turnLoop.js";
45
+ import { RaceState, type RaceStateConfig } from "../game/race.js";
43
46
  import { type CameraDirector } from "./cameraDirector.js";
44
47
  import { type InputSnapshot } from "./inputSnapshot.js";
45
48
  import { type MotionIntents } from "./motionIntents.js";
@@ -63,6 +66,8 @@ export interface GameContextEntityEntry {
63
66
  role?: CatalogEntityRole;
64
67
  /** Right-click context-menu verbs for this entity (#31). */
65
68
  verbs?: readonly ContextVerb[];
69
+ /** Purpose-specific colliders (physical body + named damage hitboxes). */
70
+ colliders?: EntityColliderSet;
66
71
  }
67
72
  export interface GameContextObjectEntry {
68
73
  proximityPrompt?: ProximityPrompt;
@@ -72,6 +77,8 @@ export interface GameContextObjectEntry {
72
77
  slotInventory?: InventoryLayout;
73
78
  /** Right-click context-menu verbs for this object (#31). */
74
79
  verbs?: readonly ContextVerb[];
80
+ colliders?: EntityColliderSet;
81
+ halfExtents?: readonly [number, number, number];
75
82
  }
76
83
  export interface GameContextContent {
77
84
  itemById?(itemId: string): GameContextItemEntry | null | undefined;
@@ -86,11 +93,14 @@ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAsset
86
93
  isNew: boolean;
87
94
  };
88
95
  now?: () => number;
96
+ occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
89
97
  }
90
98
  export interface SceneObjectContext extends ObjectStore {
91
99
  catalog(instanceId: string): GameContextObjectEntry | null;
92
100
  raycast(input: ObjectRaycastInput): ObjectRaycastHit | null;
93
101
  raycastAll(input: ObjectRaycastInput): readonly ObjectRaycastHit[];
102
+ setColliders(instanceId: string, colliders: EntityColliderSet | null): void;
103
+ collidersOf(instanceId: string): EntityColliderSet | null;
94
104
  }
95
105
  export interface FloatTextInput {
96
106
  instanceId?: string;
@@ -129,6 +139,7 @@ export interface SceneEntityContext {
129
139
  despawn(instanceId: string): boolean;
130
140
  update: EntityStore["update"];
131
141
  setPose(instanceId: string, pose: EntityPose): boolean;
142
+ setPoseConstraint: EntityStore["setPoseConstraint"];
132
143
  get(instanceId: string): SceneEntity | null;
133
144
  list(): readonly SceneEntity[];
134
145
  spawnPoseOf(instanceId: string): SpawnPose | null;
@@ -152,6 +163,9 @@ export interface SceneEntityContext {
152
163
  hasLineOfSight: SpatialApi["hasLineOfSight"];
153
164
  queryArc: SpatialApi["queryArc"];
154
165
  moveToward: SpatialApi["moveToward"];
166
+ invalidateSpatial: SpatialApi["invalidate"];
167
+ setColliders(instanceId: string, colliders: EntityColliderSet | null): void;
168
+ collidersOf(instanceId: string): EntityColliderSet | null;
155
169
  form: Forms;
156
170
  paint: PaintLayer;
157
171
  }
@@ -210,11 +224,17 @@ export interface GameContextTurn {
210
224
  /** Lazily creates (on first call, `config` required) or returns the existing notify-wrapped turn loop for `id`. */
211
225
  loop(id: string, config?: TurnLoopConfig): TurnLoop;
212
226
  }
227
+ export interface GameContextRace {
228
+ /** Lazily creates (on first call, `config` required) or returns the existing reactive race for `id` — discrete mutations and eventful `update` calls bump `ctx.version()`, so HUDs stop hand-managing `store.set` (#286.2). */
229
+ state(id: string, config?: RaceStateConfig): RaceState;
230
+ }
213
231
  export interface GameContext {
214
232
  scene: {
215
233
  object: SceneObjectContext;
216
234
  entity: SceneEntityContext;
217
235
  worldItem: SceneWorldItemContext;
236
+ raycast(input: SceneRaycastInput): SceneRaycastHit | null;
237
+ raycastAll(input: SceneRaycastInput): readonly SceneRaycastHit[];
218
238
  };
219
239
  world: GameContextWorld;
220
240
  game: {
@@ -234,6 +254,7 @@ export interface GameContext {
234
254
  store: ObservableKeyedStore<unknown>;
235
255
  cards: GameContextCards;
236
256
  turn: GameContextTurn;
257
+ race: GameContextRace;
237
258
  };
238
259
  player: {
239
260
  userId: string;