@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
@@ -30,6 +30,7 @@ import { raycastObjects, raycastObjectsAll } from "../scene/objectQuery.js";
30
30
  import { createObjectStore } from "../scene/objectStore.js";
31
31
  import { createRoster } from "../scene/roster.js";
32
32
  import { createPossession } from "../scene/possession.js";
33
+ import { createSceneRaycast, } from "../scene/sceneRaycast.js";
33
34
  import { createSpatialApi } from "../scene/spatial.js";
34
35
  import { createTargeting } from "../scene/targeting.js";
35
36
  import { createStats } from "../stats/statModifiers.js";
@@ -37,12 +38,14 @@ import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
37
38
  import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
38
39
  import { createSimClock } from "../time/simClock.js";
39
40
  import { createTurnLoop } from "../turn/turnLoop.js";
41
+ import { RaceState } from "../game/race.js";
40
42
  import { createCameraDirector } from "./cameraDirector.js";
41
43
  import { createInputSnapshot } from "./inputSnapshot.js";
42
44
  import { createMotionIntents } from "./motionIntents.js";
43
45
  export function createGameContext(options) {
44
46
  const { definition, content, player } = options;
45
47
  const now = options.now ?? Date.now;
48
+ const occluder = options.occluder;
46
49
  const signal = createChangeSignal();
47
50
  const time = createSimClock({ config: definition.time, onChange: signal.notify });
48
51
  const ground = groundFieldFor(definition.world);
@@ -68,12 +71,46 @@ export function createGameContext(options) {
68
71
  const object = objects.get(instanceId);
69
72
  return object === null ? undefined : content.objectById?.(object.catalogId);
70
73
  }
74
+ let spatialGeneration = 0;
75
+ const candidateIds = [];
76
+ let candidateIdsDirty = true;
77
+ function refreshCandidateIds() {
78
+ if (!candidateIdsDirty)
79
+ return candidateIds;
80
+ candidateIds.length = 0;
81
+ for (const entity of entities.list())
82
+ candidateIds.push(entity.id);
83
+ candidateIdsDirty = false;
84
+ return candidateIds;
85
+ }
71
86
  const spatial = createSpatialApi({
72
87
  resolvePosition: (instanceId) => entities.get(instanceId)?.position,
73
- candidates: () => entities.list().map((entity) => entity.id),
88
+ candidates: refreshCandidateIds,
89
+ grid: { cellSize: 8 },
90
+ getVersion: () => spatialGeneration,
91
+ ...(occluder !== undefined ? { occluder } : {}),
92
+ });
93
+ entities.subscribe(() => {
94
+ candidateIdsDirty = true;
95
+ spatialGeneration += 1;
96
+ spatial.invalidate();
74
97
  });
98
+ const entityColliders = new Map();
99
+ const objectColliders = new Map();
100
+ function entityCollidersOf(instanceId) {
101
+ const override = entityColliders.get(instanceId);
102
+ if (override !== undefined)
103
+ return override;
104
+ return catalogEntry(instanceId)?.colliders ?? null;
105
+ }
106
+ function objectCollidersOf(instanceId) {
107
+ const override = objectColliders.get(instanceId);
108
+ if (override !== undefined)
109
+ return override;
110
+ return catalogObject(instanceId)?.colliders ?? null;
111
+ }
75
112
  const targeting = notifyAfter(createTargeting({
76
- candidates: () => entities.list().map((entity) => entity.id),
113
+ candidates: () => [...refreshCandidateIds()],
77
114
  classify(_fromId, toId) {
78
115
  const role = catalogEntry(toId)?.role;
79
116
  return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
@@ -82,9 +119,43 @@ export function createGameContext(options) {
82
119
  }), ["setTarget", "cycleTarget"], signal.notify);
83
120
  const combatSpatial = {
84
121
  inRadius: (center, radius) => spatial.inRadius(center, radius),
85
- hasLineOfSight: (from, to) => typeof from === "string" ? spatial.hasLineOfSight(from, to) : entities.get(to) !== null,
122
+ hasLineOfSight: (from, to) => {
123
+ if (typeof from === "string")
124
+ return spatial.hasLineOfSight(from, to);
125
+ const toPos = entities.get(to)?.position;
126
+ if (toPos === undefined)
127
+ return false;
128
+ if (occluder === undefined)
129
+ return true;
130
+ return !occluder(from, toPos);
131
+ },
86
132
  positionOf: (instanceId) => entities.get(instanceId)?.position,
87
133
  };
134
+ const sceneRaycast = createSceneRaycast({
135
+ entities: {
136
+ list: () => entities.list().map((entity) => ({
137
+ id: entity.id,
138
+ position: entity.position,
139
+ rotationY: entity.rotationY,
140
+ name: entity.name,
141
+ })),
142
+ collidersOf: entityCollidersOf,
143
+ inRadius: (center, radius) => spatial.inRadius(center, radius),
144
+ get: (id) => {
145
+ const entity = entities.get(id);
146
+ if (entity === null)
147
+ return null;
148
+ return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
149
+ },
150
+ },
151
+ objects: {
152
+ list: () => objects.list(),
153
+ inBox: (min, max) => objects.inBox(min, max),
154
+ collidersOf: objectCollidersOf,
155
+ halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
156
+ },
157
+ terrain: ground,
158
+ });
88
159
  const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
89
160
  const rawEvents = createGameEvents();
90
161
  const events = {
@@ -282,6 +353,7 @@ export function createGameContext(options) {
282
353
  statsByInstance.delete(instanceId);
283
354
  targeting.clearAll(instanceId);
284
355
  pose.clear(instanceId);
356
+ entityColliders.delete(instanceId);
285
357
  return existed;
286
358
  }
287
359
  function resetAllToSpawn(filter) {
@@ -422,13 +494,14 @@ export function createGameContext(options) {
422
494
  if (input.dir !== undefined)
423
495
  telegraphEvent.dir = input.dir;
424
496
  events.emit("combat.telegraph", telegraphEvent);
497
+ const cancelVisual = () => events.emit("combat.telegraphCancelled", { id });
425
498
  const bound = input.effect;
426
499
  if (bound === undefined)
427
- return () => { };
500
+ return cancelVisual;
428
501
  const config = { shape: input.shape, at: input.at, windupMs: input.windupMs };
429
502
  if (input.dir !== undefined)
430
503
  config.dir = input.dir;
431
- return time.after(input.windupMs / 1000, () => {
504
+ const cancelEffect = time.after(input.windupMs / 1000, () => {
432
505
  const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position));
433
506
  for (const target of targets) {
434
507
  applyEffectAndFloat({
@@ -439,6 +512,10 @@ export function createGameContext(options) {
439
512
  });
440
513
  }
441
514
  });
515
+ return () => {
516
+ cancelEffect();
517
+ cancelVisual();
518
+ };
442
519
  }
443
520
  function applyHitReaction(input) {
444
521
  const attacker = entities.get(input.from);
@@ -499,10 +576,51 @@ export function createGameContext(options) {
499
576
  preview: effects.preview,
500
577
  applyEffect: applyEffectAndFloat,
501
578
  };
579
+ const projectileObstacles = definition.physics?.projectileObstacles === true;
580
+ const projectileSceneRaycast = {
581
+ raycast(input) {
582
+ return sceneRaycast.raycast({
583
+ ...input,
584
+ filter: {
585
+ entities: true,
586
+ objects: projectileObstacles,
587
+ terrain: false,
588
+ walls: projectileObstacles,
589
+ ...input.filter,
590
+ },
591
+ });
592
+ },
593
+ raycastAll(input) {
594
+ return sceneRaycast.raycastAll({
595
+ ...input,
596
+ filter: {
597
+ entities: true,
598
+ objects: projectileObstacles,
599
+ terrain: false,
600
+ walls: projectileObstacles,
601
+ ...input.filter,
602
+ },
603
+ });
604
+ },
605
+ };
502
606
  const projectiles = notifyAfter(createProjectileSystem({
503
607
  effects: floatingEffects,
504
608
  spatial: combatSpatial,
505
609
  getStat: weapon.getStat,
610
+ sceneRaycast: projectileSceneRaycast,
611
+ objects: projectileObstacles
612
+ ? {
613
+ list: () => objects.list(),
614
+ inBox: (min, max) => objects.inBox(min, max),
615
+ halfExtents: (catalogId) => {
616
+ const half = content.objectById?.(catalogId)?.halfExtents;
617
+ return half === undefined ? null : [half[0], half[1], half[2]];
618
+ },
619
+ collidersOf: objectCollidersOf,
620
+ }
621
+ : undefined,
622
+ entityCollidersOf,
623
+ rotationYOf: (instanceId) => entities.get(instanceId)?.rotationY,
506
624
  now,
507
625
  onSettle(report) {
508
626
  events.emit("projectile.settled", {
@@ -517,8 +635,16 @@ export function createGameContext(options) {
517
635
  const sceneObjects = {
518
636
  ...objects,
519
637
  catalog: (instanceId) => catalogObject(instanceId) ?? null,
520
- raycast: (input) => raycastObjects(objects.list(), input),
521
- raycastAll: (input) => raycastObjectsAll(objects.list(), input),
638
+ raycast: (input) => raycastObjects(objects, input),
639
+ raycastAll: (input) => raycastObjectsAll(objects, input),
640
+ setColliders(instanceId, colliders) {
641
+ if (colliders === null)
642
+ objectColliders.delete(instanceId);
643
+ else
644
+ objectColliders.set(instanceId, colliders);
645
+ signal.notify();
646
+ },
647
+ collidersOf: objectCollidersOf,
522
648
  };
523
649
  const store = notifyAfter(createObservableKeyedStore(), ["set", "delete"], signal.notify);
524
650
  const cardPiles = new Map();
@@ -542,7 +668,7 @@ export function createGameContext(options) {
542
668
  throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`);
543
669
  }
544
670
  const raw = createTurnLoop(config);
545
- const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "rewind", "clear"], signal.notify);
671
+ const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "discard", "clear"], signal.notify);
546
672
  const wrapped = {
547
673
  ...notifyAfter(raw, [
548
674
  "setOrder",
@@ -561,7 +687,43 @@ export function createGameContext(options) {
561
687
  turnLoops.set(id, wrapped);
562
688
  return wrapped;
563
689
  }
564
- const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic"], signal.notify);
690
+ class NotifyingRaceState extends RaceState {
691
+ addRacer(racerId, startTime) {
692
+ super.addRacer(racerId, startTime);
693
+ signal.notify();
694
+ }
695
+ removeRacer(racerId) {
696
+ super.removeRacer(racerId);
697
+ signal.notify();
698
+ }
699
+ reset() {
700
+ super.reset();
701
+ signal.notify();
702
+ }
703
+ eliminate(racerId) {
704
+ super.eliminate(racerId);
705
+ signal.notify();
706
+ }
707
+ update(now, positions) {
708
+ const raceEvents = super.update(now, positions);
709
+ if (raceEvents.length > 0)
710
+ signal.notify();
711
+ return raceEvents;
712
+ }
713
+ }
714
+ const raceStates = new Map();
715
+ function raceState(id, config) {
716
+ const existing = raceStates.get(id);
717
+ if (existing !== undefined)
718
+ return existing;
719
+ if (config === undefined) {
720
+ throw new Error(`race "${id}" has not been created yet; pass a config on first access`);
721
+ }
722
+ const created = new NotifyingRaceState(config);
723
+ raceStates.set(id, created);
724
+ return created;
725
+ }
726
+ const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
565
727
  const input = createInputSnapshot();
566
728
  const motion = createMotionIntents();
567
729
  const ctx = {
@@ -572,6 +734,7 @@ export function createGameContext(options) {
572
734
  despawn: despawnEntity,
573
735
  update: entities.update,
574
736
  setPose: entities.setPose,
737
+ setPoseConstraint: entities.setPoseConstraint,
575
738
  get: entities.get,
576
739
  list: entities.list,
577
740
  spawnPoseOf: entities.spawnPoseOf,
@@ -595,6 +758,15 @@ export function createGameContext(options) {
595
758
  hasLineOfSight: spatial.hasLineOfSight,
596
759
  queryArc: spatial.queryArc,
597
760
  moveToward: spatial.moveToward,
761
+ invalidateSpatial: spatial.invalidate,
762
+ setColliders(instanceId, colliders) {
763
+ if (colliders === null)
764
+ entityColliders.delete(instanceId);
765
+ else
766
+ entityColliders.set(instanceId, colliders);
767
+ signal.notify();
768
+ },
769
+ collidersOf: entityCollidersOf,
598
770
  form: forms,
599
771
  paint: paintLayer,
600
772
  },
@@ -606,6 +778,8 @@ export function createGameContext(options) {
606
778
  pickup: pickupWorldItem,
607
779
  consume: worldItems.remove,
608
780
  },
781
+ raycast: (input) => sceneRaycast.raycast(input),
782
+ raycastAll: (input) => sceneRaycast.raycastAll(input),
609
783
  },
610
784
  world: {
611
785
  ground,
@@ -649,6 +823,7 @@ export function createGameContext(options) {
649
823
  store,
650
824
  cards: { pile },
651
825
  turn: { loop },
826
+ race: { state: raceState },
652
827
  },
653
828
  player: {
654
829
  userId: player.userId,
@@ -5,7 +5,7 @@ import { type GameRuntimeSnapshot, type RuntimeChunkRow, type RuntimePlayerRow,
5
5
  export type ServerLoopHooks = {
6
6
  onInit?: (ctx: RuntimeInitContext) => void;
7
7
  onNewPlayer?: (ctx: RuntimeLoopContext) => void;
8
- onTick?: (ctx: RuntimeLoopContext, dtSeconds: number) => void;
8
+ onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void;
9
9
  };
10
10
  export type RuntimeInitContext = {
11
11
  snapshot: GameRuntimeSnapshot;
@@ -17,6 +17,9 @@ export type RuntimeLoopContext = RuntimeInitContext & {
17
17
  isNew: boolean;
18
18
  };
19
19
  };
20
+ export type RuntimeWorldContext = RuntimeInitContext & {
21
+ playerIds: readonly string[];
22
+ };
20
23
  export type GameRuntimeDefinition = {
21
24
  gameId: string;
22
25
  save: SaveConfig;
@@ -29,6 +32,7 @@ export type HydrateInput = {
29
32
  serverRow: RuntimeServerRow;
30
33
  playersByUserId: Record<string, RuntimePlayerRow>;
31
34
  chunksByKey: Record<string, RuntimeChunkRow>;
35
+ revision?: number;
32
36
  };
33
37
  export type GameRuntime = {
34
38
  gameId: string;
@@ -13,6 +13,7 @@ export function createGameRuntime(definition) {
13
13
  server: input.serverRow,
14
14
  players: input.playersByUserId,
15
15
  chunks: input.chunksByKey,
16
+ revision: input.revision,
16
17
  });
17
18
  if (loop?.onInit && !initialized) {
18
19
  initialized = true;
@@ -34,18 +35,18 @@ export function createGameRuntime(definition) {
34
35
  if (!loop?.onTick)
35
36
  return snapshot;
36
37
  let current = snapshot;
37
- for (const userId of Object.keys(current.players)) {
38
- const ctx = {
39
- get snapshot() {
40
- return current;
41
- },
42
- player: { userId, isNew: false },
43
- setSnapshot(next) {
44
- current = next;
45
- },
46
- };
47
- loop.onTick(ctx, dtSeconds);
48
- }
38
+ const ctx = {
39
+ get snapshot() {
40
+ return current;
41
+ },
42
+ get playerIds() {
43
+ return Object.keys(current.players);
44
+ },
45
+ setSnapshot(next) {
46
+ current = next;
47
+ },
48
+ };
49
+ loop.onTick(ctx, dtSeconds);
49
50
  return current;
50
51
  },
51
52
  joinPlayer(snapshot, userId, isNew) {
@@ -63,6 +63,7 @@ export declare function drainPendingLeaderboardIncrements(session: Record<string
63
63
  session: Record<string, unknown>;
64
64
  };
65
65
  export declare const LEADERBOARD_TOP_LIMIT = 100;
66
+ export declare function clampLimit(value: number | undefined, fallback: number, max: number): number;
66
67
  export type LeaderboardRow = {
67
68
  gameId: string;
68
69
  stat: string;
@@ -97,7 +98,12 @@ export type ServerListing = {
97
98
  tags?: string[];
98
99
  updatedAt: number;
99
100
  };
100
- export declare function toServerListing(record: GameServerRecord): ServerListing;
101
+ export type ToServerListingOptions = {
102
+ includeJoinCode?: boolean;
103
+ };
104
+ export declare function toServerListing(record: GameServerRecord, options?: ToServerListingOptions): ServerListing;
105
+ export declare const OPEN_SERVER_LISTING_LIMIT = 20;
106
+ export declare const OPEN_SERVER_LISTING_MAX = 100;
101
107
  export declare function toOpenServerListings(listings: Iterable<ServerListing>, limit?: number): ServerListing[];
102
108
  export type HostPersistence = {
103
109
  savePlan?: (plan: ServerPersistPlan) => Promise<void>;
@@ -112,6 +118,7 @@ export type HostPersistence = {
112
118
  saveProfile: (record: PlayerProfileRecord) => Promise<void>;
113
119
  loadChunks: (serverId: string) => Promise<WorldChunkRecord[]>;
114
120
  saveChunks: (serverId: string, chunks: WorldChunkRecord[]) => Promise<void>;
121
+ deleteChunks: (serverId: string, chunkKeys: string[]) => Promise<void>;
115
122
  loadFeed: (args: {
116
123
  serverId: string;
117
124
  action: string;
@@ -139,6 +146,7 @@ export type ServerPersistPlan = {
139
146
  server: GameServerRecord;
140
147
  profiles: PlayerProfileRecord[];
141
148
  chunks: WorldChunkRecord[];
149
+ deletedChunks: string[];
142
150
  leaderboard: LeaderboardIncrement[];
143
151
  };
144
152
  export declare function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan;
@@ -21,6 +21,12 @@ export function drainPendingLeaderboardIncrements(session) {
21
21
  return { increments: raw.filter(isLeaderboardIncrement), session: rest };
22
22
  }
23
23
  export const LEADERBOARD_TOP_LIMIT = 100;
24
+ export function clampLimit(value, fallback, max) {
25
+ const candidate = value ?? fallback;
26
+ if (!Number.isFinite(candidate))
27
+ return fallback;
28
+ return Math.min(Math.max(Math.floor(candidate), 1), max);
29
+ }
24
30
  export function leaderboardRowKey(row) {
25
31
  return [row.gameId, row.scope, row.stat, row.serverId ?? "", row.userId].join("|");
26
32
  }
@@ -45,7 +51,7 @@ export function applyLeaderboardRows(rows, gameId, entries, now) {
45
51
  }
46
52
  }
47
53
  export function topLeaderboardRows(rows, args) {
48
- const limit = Math.min(Math.max(args.limit ?? LEADERBOARD_TOP_LIMIT, 1), LEADERBOARD_TOP_LIMIT);
54
+ const limit = clampLimit(args.limit, LEADERBOARD_TOP_LIMIT, LEADERBOARD_TOP_LIMIT);
49
55
  return [...rows]
50
56
  .filter((row) => row.gameId === args.gameId &&
51
57
  row.stat === args.stat &&
@@ -79,7 +85,7 @@ export function shouldAutoSave(save, dirtyAt, lastSavedAt, now) {
79
85
  const anchor = lastSavedAt ?? 0;
80
86
  return now - anchor >= intervalMs;
81
87
  }
82
- export function toServerListing(record) {
88
+ export function toServerListing(record, options = {}) {
83
89
  return {
84
90
  serverId: record.serverId,
85
91
  status: record.status,
@@ -88,16 +94,20 @@ export function toServerListing(record) {
88
94
  mode: record.mode,
89
95
  label: record.label,
90
96
  visibility: record.visibility,
91
- joinCode: record.joinCode,
97
+ ...(options.includeJoinCode === true && record.joinCode !== undefined
98
+ ? { joinCode: record.joinCode }
99
+ : {}),
92
100
  tags: record.tags,
93
101
  updatedAt: record.updatedAt,
94
102
  };
95
103
  }
96
- export function toOpenServerListings(listings, limit = 20) {
104
+ export const OPEN_SERVER_LISTING_LIMIT = 20;
105
+ export const OPEN_SERVER_LISTING_MAX = 100;
106
+ export function toOpenServerListings(listings, limit = OPEN_SERVER_LISTING_LIMIT) {
97
107
  return [...listings]
98
108
  .filter((listing) => listing.status === "running")
99
109
  .sort((a, b) => b.updatedAt - a.updatedAt)
100
- .slice(0, limit);
110
+ .slice(0, clampLimit(limit, OPEN_SERVER_LISTING_LIMIT, OPEN_SERVER_LISTING_MAX));
101
111
  }
102
112
  export function buildHydratePlayers(server, profiles) {
103
113
  const players = {};
@@ -146,12 +156,16 @@ export function planServerPersist(server, snapshot, save, now) {
146
156
  }
147
157
  }
148
158
  const chunks = [];
159
+ const deletedChunks = [];
149
160
  if (isSaveEnabled(save) && saveScopeIncludesChunks(save.scope)) {
150
161
  for (const chunkKey of snapshot.dirty.chunks) {
151
162
  const chunk = snapshot.chunks[chunkKey];
152
- if (!chunk)
153
- continue;
154
- chunks.push({ serverId: server.serverId, chunkKey, snapshot: chunk, updatedAt: now });
163
+ if (chunk) {
164
+ chunks.push({ serverId: server.serverId, chunkKey, snapshot: chunk, updatedAt: now });
165
+ }
166
+ else {
167
+ deletedChunks.push(chunkKey);
168
+ }
155
169
  }
156
170
  }
157
171
  return {
@@ -166,6 +180,7 @@ export function planServerPersist(server, snapshot, save, now) {
166
180
  },
167
181
  profiles,
168
182
  chunks,
183
+ deletedChunks,
169
184
  leaderboard,
170
185
  };
171
186
  }
@@ -1,7 +1,12 @@
1
+ import type { PointerAxisState } from "../input/pointerAxis.js";
1
2
  export interface InputSnapshot {
2
3
  /** Replaces the held-action set for this frame. Called by the shell before `onTick` each frame; does not bump `ctx.version()` or notify `ctx.subscribe` listeners — per-frame publishes would storm subscribers. */
3
4
  publish(held: readonly string[]): void;
5
+ /** Replaces the normalized pointer-position state for this frame (#293). Same no-notify contract as `publish`. */
6
+ publishPointer(state: PointerAxisState | null): void;
4
7
  isDown(action: string): boolean;
5
8
  held(): readonly string[];
9
+ /** Pointer position over the play surface, `[-1, 1]` per axis with `+y` down, published by the shell each frame; `null` until the first pointer move. */
10
+ pointer(): PointerAxisState | null;
6
11
  }
7
12
  export declare function createInputSnapshot(): InputSnapshot;
@@ -1,12 +1,17 @@
1
1
  export function createInputSnapshot() {
2
2
  let heldSet = new Set();
3
3
  let heldList = [];
4
+ let pointerState = null;
4
5
  return {
5
6
  publish(held) {
6
7
  heldList = held;
7
8
  heldSet = new Set(held);
8
9
  },
10
+ publishPointer(state) {
11
+ pointerState = state;
12
+ },
9
13
  isDown: (action) => heldSet.has(action),
10
14
  held: () => heldList,
15
+ pointer: () => pointerState,
11
16
  };
12
17
  }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Seam for game code to reach the vertical motion the shell's FrameDriver
2
+ * Seam for game code to reach the motion the shell's FrameDriver
3
3
  * otherwise owns privately (#162.4). Game code calls `impulse`,
4
- * `setVerticalVelocity`, and/or `setY` from `onTick` or commands; the shell
5
- * calls `takePending()` once per frame, before integrating gravity, to drain
6
- * what accumulated. `setY` wins over physics for that frame; impulses add to
7
- * the vertical velocity the driver is about to integrate; a later
8
- * `setVerticalVelocity` replaces that velocity outright.
4
+ * `pushHorizontal`, `setVerticalVelocity`, and/or `setY` from `onTick` or
5
+ * commands; the shell calls `takePending()` once per frame, before
6
+ * integrating gravity, to drain what accumulated. `setY` wins over physics
7
+ * for that frame; impulses add to the velocity the driver is about to
8
+ * integrate; a later `setVerticalVelocity` replaces that velocity outright.
9
+ * Horizontal pushes compose with the walk controller (#282.4): they add to
10
+ * its horizontal velocity and decay naturally as it re-blends toward input —
11
+ * knockback, dashes, explosion shoves without raw `setPose` offsets.
9
12
  *
10
13
  * Intentionally not `ctx.version()`/`ctx.subscribe` wired: these fire at
11
14
  * most once per frame and would otherwise storm subscribers with per-frame
@@ -13,13 +16,18 @@
13
16
  */
14
17
  export interface MotionIntents {
15
18
  impulse(velocityY: number): void;
19
+ /** Adds a horizontal velocity kick (world units/s) that the walk controller integrates and decays. */
20
+ pushHorizontal(velocityX: number, velocityZ: number): void;
16
21
  setVerticalVelocity(velocityY: number): void;
17
22
  setY(y: number): void;
18
23
  takePending(): MotionIntentBatch | null;
19
24
  }
20
25
  export interface MotionIntentBatch {
21
26
  impulses: readonly number[];
27
+ horizontalImpulses: readonly (readonly [number, number])[];
22
28
  verticalVelocity: number | null;
23
29
  y: number | null;
24
30
  }
25
31
  export declare function createMotionIntents(): MotionIntents;
32
+ /** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
33
+ export declare function applyHorizontalImpulses(velocityX: number, velocityZ: number, batch: MotionIntentBatch | null): readonly [number, number];
@@ -1,11 +1,15 @@
1
1
  export function createMotionIntents() {
2
2
  let impulses = [];
3
+ let horizontalImpulses = [];
3
4
  let verticalVelocity = null;
4
5
  let y = null;
5
6
  return {
6
7
  impulse(velocityY) {
7
8
  impulses.push(velocityY);
8
9
  },
10
+ pushHorizontal(velocityX, velocityZ) {
11
+ horizontalImpulses.push([velocityX, velocityZ]);
12
+ },
9
13
  setVerticalVelocity(velocityY) {
10
14
  verticalVelocity = velocityY;
11
15
  },
@@ -13,13 +17,27 @@ export function createMotionIntents() {
13
17
  y = nextY;
14
18
  },
15
19
  takePending() {
16
- if (impulses.length === 0 && verticalVelocity === null && y === null)
20
+ if (impulses.length === 0 && horizontalImpulses.length === 0 && verticalVelocity === null && y === null) {
17
21
  return null;
18
- const batch = { impulses, verticalVelocity, y };
22
+ }
23
+ const batch = { impulses, horizontalImpulses, verticalVelocity, y };
19
24
  impulses = [];
25
+ horizontalImpulses = [];
20
26
  verticalVelocity = null;
21
27
  y = null;
22
28
  return batch;
23
29
  },
24
30
  };
25
31
  }
32
+ /** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
33
+ export function applyHorizontalImpulses(velocityX, velocityZ, batch) {
34
+ if (batch === null || batch.horizontalImpulses.length === 0)
35
+ return [velocityX, velocityZ];
36
+ let x = velocityX;
37
+ let z = velocityZ;
38
+ for (const [ix, iz] of batch.horizontalImpulses) {
39
+ x += ix;
40
+ z += iz;
41
+ }
42
+ return [x, z];
43
+ }
@@ -77,6 +77,7 @@ export declare function createRuntimeSnapshot(args: {
77
77
  server?: RuntimeServerRow;
78
78
  players?: Record<string, RuntimePlayerRow>;
79
79
  chunks?: Record<string, RuntimeChunkRow>;
80
+ revision?: number;
80
81
  }): GameRuntimeSnapshot;
81
82
  export declare function markServerDirty(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot;
82
83
  export declare function markPlayerDirty(snapshot: GameRuntimeSnapshot, userId: string): GameRuntimeSnapshot;
@@ -24,7 +24,7 @@ export function createRuntimeSnapshot(args) {
24
24
  server: args.server ?? createEmptyServerRow(),
25
25
  players: args.players ?? {},
26
26
  chunks: args.chunks ?? {},
27
- revision: 0,
27
+ revision: args.revision ?? 0,
28
28
  dirty: {
29
29
  server: false,
30
30
  players: [],