@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -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.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -24,26 +24,34 @@ import { createItemUse, } from "../item/use.js";
24
24
  import { createWeaponStats } from "../item/weapon.js";
25
25
  import { createPoseState } from "../movement/poseState.js";
26
26
  import { createPaintLayer } from "../scene/paintLayer.js";
27
- import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
27
+ import { createEntityStatsApi, hydrateEntityStats, seedStatValues, setStatValue, snapshotEntityStats, } from "../scene/entityStats.js";
28
28
  import { createForms } from "../scene/form.js";
29
+ import { scaledObjectColliders } from "../scene/colliders.js";
29
30
  import { raycastObjects, raycastObjectsAll } from "../scene/objectQuery.js";
30
- import { createObjectStore } from "../scene/objectStore.js";
31
+ import { createObjectStore, objectVisualScale } from "../scene/objectStore.js";
31
32
  import { createRoster } from "../scene/roster.js";
33
+ import { createConnectedPlayers } from "../game/connectedPlayers.js";
32
34
  import { createPossession } from "../scene/possession.js";
35
+ import { createSceneRaycast, } from "../scene/sceneRaycast.js";
33
36
  import { createSpatialApi } from "../scene/spatial.js";
34
37
  import { createTargeting } from "../scene/targeting.js";
35
38
  import { createStats } from "../stats/statModifiers.js";
36
39
  import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
37
40
  import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
41
+ import { applyWorldSnapshot, composeWorldSnapshot, } from "./worldSnapshot.js";
38
42
  import { createSimClock } from "../time/simClock.js";
39
43
  import { createTurnLoop } from "../turn/turnLoop.js";
44
+ import { RaceState } from "../game/race.js";
40
45
  import { createCameraDirector } from "./cameraDirector.js";
41
46
  import { createInputSnapshot } from "./inputSnapshot.js";
42
47
  import { createMotionIntents } from "./motionIntents.js";
43
48
  export function createGameContext(options) {
44
49
  const { definition, content, player } = options;
45
50
  const now = options.now ?? Date.now;
51
+ const occluder = options.occluder;
46
52
  const signal = createChangeSignal();
53
+ let actingUserId = null;
54
+ const activeUserId = () => actingUserId ?? player.userId;
47
55
  const time = createSimClock({ config: definition.time, onChange: signal.notify });
48
56
  const ground = groundFieldFor(definition.world);
49
57
  const entities = definition.scene;
@@ -68,12 +76,54 @@ export function createGameContext(options) {
68
76
  const object = objects.get(instanceId);
69
77
  return object === null ? undefined : content.objectById?.(object.catalogId);
70
78
  }
79
+ let spatialGeneration = 0;
80
+ const candidateIds = [];
81
+ let candidateIdsDirty = true;
82
+ function refreshCandidateIds() {
83
+ if (!candidateIdsDirty)
84
+ return candidateIds;
85
+ candidateIds.length = 0;
86
+ for (const entity of entities.list())
87
+ candidateIds.push(entity.id);
88
+ candidateIdsDirty = false;
89
+ return candidateIds;
90
+ }
71
91
  const spatial = createSpatialApi({
72
92
  resolvePosition: (instanceId) => entities.get(instanceId)?.position,
73
- candidates: () => entities.list().map((entity) => entity.id),
93
+ candidates: refreshCandidateIds,
94
+ grid: { cellSize: 8 },
95
+ getVersion: () => spatialGeneration,
96
+ ...(occluder !== undefined ? { occluder } : {}),
97
+ });
98
+ entities.subscribe(() => {
99
+ candidateIdsDirty = true;
100
+ spatialGeneration += 1;
101
+ spatial.invalidate();
74
102
  });
103
+ const entityColliders = new Map();
104
+ const objectColliders = new Map();
105
+ function entityCollidersOf(instanceId) {
106
+ const override = entityColliders.get(instanceId);
107
+ if (override !== undefined)
108
+ return override;
109
+ return catalogEntry(instanceId)?.colliders ?? null;
110
+ }
111
+ function objectCollidersOf(instanceId) {
112
+ const override = objectColliders.get(instanceId);
113
+ if (override !== undefined)
114
+ return override;
115
+ const catalog = catalogObject(instanceId);
116
+ if (catalog?.colliders !== undefined)
117
+ return catalog.colliders;
118
+ if (catalog?.halfExtents !== undefined)
119
+ return null;
120
+ const object = objects.get(instanceId);
121
+ if (object === null || object.visual?.scale === undefined)
122
+ return null;
123
+ return scaledObjectColliders(objectVisualScale(object.visual));
124
+ }
75
125
  const targeting = notifyAfter(createTargeting({
76
- candidates: () => entities.list().map((entity) => entity.id),
126
+ candidates: () => [...refreshCandidateIds()],
77
127
  classify(_fromId, toId) {
78
128
  const role = catalogEntry(toId)?.role;
79
129
  return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
@@ -82,9 +132,43 @@ export function createGameContext(options) {
82
132
  }), ["setTarget", "cycleTarget"], signal.notify);
83
133
  const combatSpatial = {
84
134
  inRadius: (center, radius) => spatial.inRadius(center, radius),
85
- hasLineOfSight: (from, to) => typeof from === "string" ? spatial.hasLineOfSight(from, to) : entities.get(to) !== null,
135
+ hasLineOfSight: (from, to) => {
136
+ if (typeof from === "string")
137
+ return spatial.hasLineOfSight(from, to);
138
+ const toPos = entities.get(to)?.position;
139
+ if (toPos === undefined)
140
+ return false;
141
+ if (occluder === undefined)
142
+ return true;
143
+ return !occluder(from, toPos);
144
+ },
86
145
  positionOf: (instanceId) => entities.get(instanceId)?.position,
87
146
  };
147
+ const sceneRaycast = createSceneRaycast({
148
+ entities: {
149
+ list: () => entities.list().map((entity) => ({
150
+ id: entity.id,
151
+ position: entity.position,
152
+ rotationY: entity.rotationY,
153
+ name: entity.name,
154
+ })),
155
+ collidersOf: entityCollidersOf,
156
+ inRadius: (center, radius) => spatial.inRadius(center, radius),
157
+ get: (id) => {
158
+ const entity = entities.get(id);
159
+ if (entity === null)
160
+ return null;
161
+ return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
162
+ },
163
+ },
164
+ objects: {
165
+ list: () => objects.list(),
166
+ inBox: (min, max) => objects.inBox(min, max),
167
+ collidersOf: objectCollidersOf,
168
+ halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
169
+ },
170
+ terrain: ground,
171
+ });
88
172
  const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
89
173
  const rawEvents = createGameEvents();
90
174
  const events = {
@@ -98,34 +182,55 @@ export function createGameContext(options) {
98
182
  const feed = createGameFeed(definition.feed);
99
183
  const lootRegistry = createLootRegistry();
100
184
  const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], signal.notify);
101
- const rawSocial = createSocial({
102
- events,
103
- now,
104
- emotes: {
105
- entities: { get: (id) => entities.get(id) },
106
- spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
107
- },
108
- });
109
- const social = {
110
- friends: notifyAfter(rawSocial.friends, ["request", "accept", "decline", "remove", "block", "hydrate"], signal.notify),
111
- party: notifyAfter(rawSocial.party, ["invite", "accept", "decline", "kick", "leave", "promote"], signal.notify),
112
- presence: rawSocial.presence,
113
- emotes: rawSocial.emotes,
114
- worldInvites: notifyAfter(rawSocial.worldInvites, ["invite", "accept", "decline"], signal.notify),
115
- };
116
- const chat = notifyAfter(createChat({
117
- events,
118
- now,
119
- party: rawSocial.party,
120
- proximity: {
121
- entities: { get: (id) => entities.get(id) },
122
- spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
123
- },
124
- blockedBy: (userId) => rawSocial.friends.snapshot(userId).blocked,
125
- }), ["register", "send", "whisper", "hydrate"], signal.notify);
126
- const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify);
127
- const roster = notifyAfter(createRoster({ now }), ["capture", "release", "setEquipped", "hydrate"], signal.notify);
128
- const playerStats = createStats({});
185
+ const features = definition.features ?? {};
186
+ const rawSocial = features.social || features.chat
187
+ ? createSocial({
188
+ events,
189
+ now,
190
+ emotes: {
191
+ entities: { get: (id) => entities.get(id) },
192
+ spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
193
+ },
194
+ })
195
+ : null;
196
+ let social;
197
+ if (features.social && rawSocial !== null) {
198
+ social = {
199
+ friends: notifyAfter(rawSocial.friends, ["request", "accept", "decline", "remove", "block", "hydrate"], signal.notify),
200
+ party: notifyAfter(rawSocial.party, ["invite", "accept", "decline", "kick", "leave", "promote"], signal.notify),
201
+ presence: rawSocial.presence,
202
+ emotes: rawSocial.emotes,
203
+ worldInvites: notifyAfter(rawSocial.worldInvites, ["invite", "accept", "decline"], signal.notify),
204
+ };
205
+ }
206
+ let chat;
207
+ if (features.chat && rawSocial !== null) {
208
+ chat = notifyAfter(createChat({
209
+ events,
210
+ now,
211
+ party: rawSocial.party,
212
+ proximity: {
213
+ entities: { get: (id) => entities.get(id) },
214
+ spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
215
+ },
216
+ blockedBy: (userId) => rawSocial.friends.snapshot(userId).blocked,
217
+ }), ["register", "send", "whisper", "hydrate"], signal.notify);
218
+ }
219
+ const leaderboard = features.leaderboard
220
+ ? notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify)
221
+ : undefined;
222
+ const roster = features.roster
223
+ ? notifyAfter(createRoster({ now }), ["capture", "release", "setEquipped", "hydrate"], signal.notify)
224
+ : undefined;
225
+ const playerStatsByUser = new Map();
226
+ function playerStatsFor(userId) {
227
+ let stats = playerStatsByUser.get(userId);
228
+ if (stats === undefined) {
229
+ stats = createStats({});
230
+ playerStatsByUser.set(userId, stats);
231
+ }
232
+ return stats;
233
+ }
129
234
  const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
130
235
  const commandRegistry = createCommandRegistry();
131
236
  const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
@@ -140,7 +245,15 @@ export function createGameContext(options) {
140
245
  layouts[inventoryId] = { slots: declaration.slots, accepts: declaration.accepts };
141
246
  }
142
247
  const traits = Object.values(inventoryDeclarations).find((declaration) => declaration.traits !== undefined)?.traits ?? { stackLimit: () => Number.POSITIVE_INFINITY };
143
- const inventory = notifyAfter(createInventorySet(layouts, traits), ["put", "take", "move", "replaceState"], signal.notify);
248
+ const inventoryByUser = new Map();
249
+ function inventoryFor(userId) {
250
+ let set = inventoryByUser.get(userId);
251
+ if (set === undefined) {
252
+ set = notifyAfter(createInventorySet(layouts, traits), ["put", "take", "move", "replaceState"], signal.notify);
253
+ inventoryByUser.set(userId, set);
254
+ }
255
+ return set;
256
+ }
144
257
  const wallets = new Map();
145
258
  const walletOf = (userId) => wallets.get(userId) ?? createEmptyWallet();
146
259
  const economy = {
@@ -158,7 +271,8 @@ export function createGameContext(options) {
158
271
  return null;
159
272
  },
160
273
  };
161
- function putIntoAnyInventory(itemId, count) {
274
+ function putIntoAnyInventory(userId, itemId, count) {
275
+ const inventory = inventoryFor(userId);
162
276
  for (const inventoryId of inventoryIds) {
163
277
  if (inventory.put(inventoryId, itemId, count).status === "ok")
164
278
  return;
@@ -170,7 +284,7 @@ export function createGameContext(options) {
170
284
  roll: lootRegistry.roll,
171
285
  grantToPlayer(userId, drops, source) {
172
286
  grantDrops(drops, {
173
- putItem: (itemId, count) => putIntoAnyInventory(itemId, count),
287
+ putItem: (itemId, count) => putIntoAnyInventory(userId, itemId, count),
174
288
  grantCurrency: (currencyId, amount) => economy.grant(userId, currencyId, amount),
175
289
  });
176
290
  const event = { userId, drops };
@@ -182,17 +296,17 @@ export function createGameContext(options) {
182
296
  const trade = createTradeSystem({
183
297
  resolveTrade: (itemId) => content.itemById?.(itemId)?.trade,
184
298
  wallet: {
185
- canAfford: (costs) => (walletCanAfford(walletOf(player.userId), costs) ? null : "insufficient-funds"),
299
+ canAfford: (costs) => (walletCanAfford(walletOf(activeUserId()), costs) ? null : "insufficient-funds"),
186
300
  charge(costs) {
187
- const result = walletChargeAll(walletOf(player.userId), costs);
301
+ const result = walletChargeAll(walletOf(activeUserId()), costs);
188
302
  if (result.status === "ok") {
189
- wallets.set(player.userId, result.state);
303
+ wallets.set(activeUserId(), result.state);
190
304
  signal.notify();
191
305
  }
192
306
  },
193
307
  grant(gains) {
194
308
  for (const [currencyId, amount] of Object.entries(gains)) {
195
- economy.grant(player.userId, currencyId, amount);
309
+ economy.grant(activeUserId(), currencyId, amount);
196
310
  }
197
311
  },
198
312
  },
@@ -200,16 +314,16 @@ export function createGameContext(options) {
200
314
  put(inventoryId, itemId, count) {
201
315
  if (layouts[inventoryId] === undefined)
202
316
  return { reason: `unknown inventory "${inventoryId}"` };
203
- const result = inventory.put(inventoryId, itemId, count);
317
+ const result = inventoryFor(activeUserId()).put(inventoryId, itemId, count);
204
318
  return result.status === "ok" ? null : { reason: result.reason };
205
319
  },
206
320
  take(inventoryId, itemId, count) {
207
321
  if (layouts[inventoryId] === undefined)
208
322
  return { reason: `unknown inventory "${inventoryId}"` };
209
- const result = inventory.take(inventoryId, itemId, count);
323
+ const result = inventoryFor(activeUserId()).take(inventoryId, itemId, count);
210
324
  return result.status === "ok" ? null : { reason: result.reason };
211
325
  },
212
- count: (inventoryId, itemId) => inventory.count(inventoryId, itemId),
326
+ count: (inventoryId, itemId) => inventoryFor(activeUserId()).count(inventoryId, itemId),
213
327
  },
214
328
  });
215
329
  function seedUserPool(userId, statId, pool) {
@@ -227,11 +341,9 @@ export function createGameContext(options) {
227
341
  },
228
342
  grantEconomy: (userId, currencyId, amount) => economy.grant(userId, currencyId, amount),
229
343
  grantItem(userId, inventoryId, itemId, count) {
230
- if (userId !== player.userId)
231
- return { reason: `unknown user "${userId}"` };
232
344
  if (layouts[inventoryId] === undefined)
233
345
  return { reason: `unknown inventory "${inventoryId}"` };
234
- const result = inventory.put(inventoryId, itemId, count);
346
+ const result = inventoryFor(userId).put(inventoryId, itemId, count);
235
347
  return result.status === "ok" ? null : { reason: result.reason };
236
348
  },
237
349
  grantUnlock: (userId, unlockId) => unlocks.grant(userId, unlockId),
@@ -241,7 +353,8 @@ export function createGameContext(options) {
241
353
  const quest = notifyAfter(rawQuest, ["accept", "abandon", "progress", "turnIn", "grant", "revoke", "hydrate"], signal.notify);
242
354
  const loadouts = notifyAfter(createLoadouts({
243
355
  inventory: {
244
- begin() {
356
+ begin(userId) {
357
+ const inventory = inventoryFor(userId);
245
358
  const staged = new Map();
246
359
  return {
247
360
  put(inventoryId, itemId, count, slot) {
@@ -282,6 +395,7 @@ export function createGameContext(options) {
282
395
  statsByInstance.delete(instanceId);
283
396
  targeting.clearAll(instanceId);
284
397
  pose.clear(instanceId);
398
+ entityColliders.delete(instanceId);
285
399
  return existed;
286
400
  }
287
401
  function resetAllToSpawn(filter) {
@@ -422,13 +536,14 @@ export function createGameContext(options) {
422
536
  if (input.dir !== undefined)
423
537
  telegraphEvent.dir = input.dir;
424
538
  events.emit("combat.telegraph", telegraphEvent);
539
+ const cancelVisual = () => events.emit("combat.telegraphCancelled", { id });
425
540
  const bound = input.effect;
426
541
  if (bound === undefined)
427
- return () => { };
542
+ return cancelVisual;
428
543
  const config = { shape: input.shape, at: input.at, windupMs: input.windupMs };
429
544
  if (input.dir !== undefined)
430
545
  config.dir = input.dir;
431
- return time.after(input.windupMs / 1000, () => {
546
+ const cancelEffect = time.after(input.windupMs / 1000, () => {
432
547
  const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position));
433
548
  for (const target of targets) {
434
549
  applyEffectAndFloat({
@@ -439,6 +554,10 @@ export function createGameContext(options) {
439
554
  });
440
555
  }
441
556
  });
557
+ return () => {
558
+ cancelEffect();
559
+ cancelVisual();
560
+ };
442
561
  }
443
562
  function applyHitReaction(input) {
444
563
  const attacker = entities.get(input.from);
@@ -499,10 +618,51 @@ export function createGameContext(options) {
499
618
  preview: effects.preview,
500
619
  applyEffect: applyEffectAndFloat,
501
620
  };
621
+ const projectileObstacles = definition.physics?.projectileObstacles === true;
622
+ const projectileSceneRaycast = {
623
+ raycast(input) {
624
+ return sceneRaycast.raycast({
625
+ ...input,
626
+ filter: {
627
+ entities: true,
628
+ objects: projectileObstacles,
629
+ terrain: false,
630
+ walls: projectileObstacles,
631
+ ...input.filter,
632
+ },
633
+ });
634
+ },
635
+ raycastAll(input) {
636
+ return sceneRaycast.raycastAll({
637
+ ...input,
638
+ filter: {
639
+ entities: true,
640
+ objects: projectileObstacles,
641
+ terrain: false,
642
+ walls: projectileObstacles,
643
+ ...input.filter,
644
+ },
645
+ });
646
+ },
647
+ };
502
648
  const projectiles = notifyAfter(createProjectileSystem({
503
649
  effects: floatingEffects,
504
650
  spatial: combatSpatial,
505
651
  getStat: weapon.getStat,
652
+ sceneRaycast: projectileSceneRaycast,
653
+ objects: projectileObstacles
654
+ ? {
655
+ list: () => objects.list(),
656
+ inBox: (min, max) => objects.inBox(min, max),
657
+ halfExtents: (catalogId) => {
658
+ const half = content.objectById?.(catalogId)?.halfExtents;
659
+ return half === undefined ? null : [half[0], half[1], half[2]];
660
+ },
661
+ collidersOf: objectCollidersOf,
662
+ }
663
+ : undefined,
664
+ entityCollidersOf,
665
+ rotationYOf: (instanceId) => entities.get(instanceId)?.rotationY,
506
666
  now,
507
667
  onSettle(report) {
508
668
  events.emit("projectile.settled", {
@@ -517,10 +677,21 @@ export function createGameContext(options) {
517
677
  const sceneObjects = {
518
678
  ...objects,
519
679
  catalog: (instanceId) => catalogObject(instanceId) ?? null,
520
- raycast: (input) => raycastObjects(objects.list(), input),
521
- raycastAll: (input) => raycastObjectsAll(objects.list(), input),
680
+ raycast: (input) => raycastObjects(objects, input),
681
+ raycastAll: (input) => raycastObjectsAll(objects, input),
682
+ setColliders(instanceId, colliders) {
683
+ if (colliders === null)
684
+ objectColliders.delete(instanceId);
685
+ else
686
+ objectColliders.set(instanceId, colliders);
687
+ signal.notify();
688
+ },
689
+ collidersOf: objectCollidersOf,
522
690
  };
523
- const store = notifyAfter(createObservableKeyedStore(), ["set", "delete"], signal.notify);
691
+ const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify);
692
+ const players = features.players
693
+ ? notifyAfter(createConnectedPlayers(), ["join", "leave"], signal.notify)
694
+ : undefined;
524
695
  const cardPiles = new Map();
525
696
  function pile(id, config) {
526
697
  const existing = cardPiles.get(id);
@@ -542,7 +713,7 @@ export function createGameContext(options) {
542
713
  throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`);
543
714
  }
544
715
  const raw = createTurnLoop(config);
545
- const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "rewind", "clear"], signal.notify);
716
+ const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "discard", "clear"], signal.notify);
546
717
  const wrapped = {
547
718
  ...notifyAfter(raw, [
548
719
  "setOrder",
@@ -561,9 +732,101 @@ export function createGameContext(options) {
561
732
  turnLoops.set(id, wrapped);
562
733
  return wrapped;
563
734
  }
564
- const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic"], signal.notify);
735
+ class NotifyingRaceState extends RaceState {
736
+ addRacer(racerId, startTime) {
737
+ super.addRacer(racerId, startTime);
738
+ signal.notify();
739
+ }
740
+ removeRacer(racerId) {
741
+ super.removeRacer(racerId);
742
+ signal.notify();
743
+ }
744
+ reset() {
745
+ super.reset();
746
+ signal.notify();
747
+ }
748
+ eliminate(racerId) {
749
+ super.eliminate(racerId);
750
+ signal.notify();
751
+ }
752
+ update(now, positions) {
753
+ const raceEvents = super.update(now, positions);
754
+ if (raceEvents.length > 0)
755
+ signal.notify();
756
+ return raceEvents;
757
+ }
758
+ }
759
+ const raceStates = new Map();
760
+ function raceState(id, config) {
761
+ const existing = raceStates.get(id);
762
+ if (existing !== undefined)
763
+ return existing;
764
+ if (config === undefined) {
765
+ throw new Error(`race "${id}" has not been created yet; pass a config on first access`);
766
+ }
767
+ const created = new NotifyingRaceState(config);
768
+ raceStates.set(id, created);
769
+ return created;
770
+ }
771
+ const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
565
772
  const input = createInputSnapshot();
566
- const motion = createMotionIntents();
773
+ const motionByUser = new Map();
774
+ function motionFor(userId) {
775
+ let queue = motionByUser.get(userId);
776
+ if (queue === undefined) {
777
+ queue = createMotionIntents();
778
+ motionByUser.set(userId, queue);
779
+ }
780
+ return queue;
781
+ }
782
+ const snapshotModules = [
783
+ { key: "entities", snapshot: () => entities.snapshot(), hydrate: (data) => entities.hydrate(data) },
784
+ {
785
+ key: "stats",
786
+ snapshot: () => snapshotEntityStats(statsByInstance),
787
+ hydrate: (data) => hydrateEntityStats(statsByInstance, data),
788
+ },
789
+ {
790
+ key: "store",
791
+ snapshot: () => store.snapshot(),
792
+ hydrate: (data) => store.hydrate(data),
793
+ },
794
+ { key: "feed", snapshot: () => feed.snapshot(), hydrate: (data) => feed.hydrate(data) },
795
+ {
796
+ key: "inventory",
797
+ snapshot: () => {
798
+ const byUser = {};
799
+ for (const [userId, set] of inventoryByUser) {
800
+ const states = {};
801
+ for (const inventoryId of inventoryIds)
802
+ states[inventoryId] = set.state(inventoryId);
803
+ byUser[userId] = states;
804
+ }
805
+ return byUser;
806
+ },
807
+ hydrate: (data) => {
808
+ for (const [userId, states] of Object.entries(data)) {
809
+ const set = inventoryFor(userId);
810
+ for (const [inventoryId, state] of Object.entries(states))
811
+ set.replaceState(inventoryId, state);
812
+ }
813
+ },
814
+ },
815
+ ];
816
+ if (leaderboard) {
817
+ snapshotModules.push({
818
+ key: "leaderboard",
819
+ snapshot: () => leaderboard.snapshot(),
820
+ hydrate: (data) => leaderboard.hydrate(data),
821
+ });
822
+ }
823
+ if (chat) {
824
+ snapshotModules.push({
825
+ key: "chat",
826
+ snapshot: () => chat.snapshot(),
827
+ hydrate: (data) => chat.hydrate(data),
828
+ });
829
+ }
567
830
  const ctx = {
568
831
  scene: {
569
832
  object: sceneObjects,
@@ -572,11 +835,13 @@ export function createGameContext(options) {
572
835
  despawn: despawnEntity,
573
836
  update: entities.update,
574
837
  setPose: entities.setPose,
838
+ setPoseConstraint: entities.setPoseConstraint,
575
839
  get: entities.get,
576
840
  list: entities.list,
577
841
  spawnPoseOf: entities.spawnPoseOf,
578
842
  resetToSpawn: entities.resetToSpawn,
579
843
  resetAllToSpawn,
844
+ blackboard: entities.blackboard,
580
845
  stats: entityStats,
581
846
  floatText: emitFloatText,
582
847
  telegraph: fireTelegraph,
@@ -595,6 +860,15 @@ export function createGameContext(options) {
595
860
  hasLineOfSight: spatial.hasLineOfSight,
596
861
  queryArc: spatial.queryArc,
597
862
  moveToward: spatial.moveToward,
863
+ invalidateSpatial: spatial.invalidate,
864
+ setColliders(instanceId, colliders) {
865
+ if (colliders === null)
866
+ entityColliders.delete(instanceId);
867
+ else
868
+ entityColliders.set(instanceId, colliders);
869
+ signal.notify();
870
+ },
871
+ collidersOf: entityCollidersOf,
598
872
  form: forms,
599
873
  paint: paintLayer,
600
874
  },
@@ -606,6 +880,8 @@ export function createGameContext(options) {
606
880
  pickup: pickupWorldItem,
607
881
  consume: worldItems.remove,
608
882
  },
883
+ raycast: (input) => sceneRaycast.raycast(input),
884
+ raycastAll: (input) => sceneRaycast.raycastAll(input),
609
885
  },
610
886
  world: {
611
887
  ground,
@@ -621,8 +897,27 @@ export function createGameContext(options) {
621
897
  signal.notify();
622
898
  return result;
623
899
  },
900
+ runAs(actorUserId, name, input) {
901
+ const previous = actingUserId;
902
+ actingUserId = actorUserId;
903
+ try {
904
+ const result = commandRegistry.run(ctx, name, input);
905
+ signal.notify();
906
+ return result;
907
+ }
908
+ finally {
909
+ actingUserId = previous;
910
+ }
911
+ },
912
+ actor: () => actingUserId,
624
913
  },
625
914
  events,
915
+ playEntityAnimation: (instanceId, event) => events.emit("entity.animation", { instanceId, event }),
916
+ audio: {
917
+ play: (sound, at) => events.emit("audio.play", at === undefined ? { sound } : { sound, at }),
918
+ music: (theme, transpose) => events.emit("audio.music", transpose === undefined ? { theme } : { theme, transpose }),
919
+ resume: () => events.emit("audio.resume", {}),
920
+ },
626
921
  feed: {
627
922
  bind: (action) => feed.bind(action, events),
628
923
  push(action, entry) {
@@ -647,20 +942,33 @@ export function createGameContext(options) {
647
942
  leaderboard,
648
943
  roster,
649
944
  store,
650
- cards: { pile },
651
- turn: { loop },
945
+ cards: features.cards ? { pile } : undefined,
946
+ turn: features.turn ? { loop } : undefined,
947
+ race: features.race ? { state: raceState } : undefined,
948
+ players,
652
949
  },
653
950
  player: {
654
- userId: player.userId,
951
+ get userId() {
952
+ return activeUserId();
953
+ },
655
954
  isNew: player.isNew,
656
- inventory,
657
- stats: playerStats,
955
+ get inventory() {
956
+ return inventoryFor(activeUserId());
957
+ },
958
+ inventoryFor,
959
+ get stats() {
960
+ return playerStatsFor(activeUserId());
961
+ },
962
+ statsFor: playerStatsFor,
658
963
  loadout: loadouts,
659
964
  applyLoadout: loadouts.applyLoadout,
660
965
  movement: pose,
661
966
  possession,
662
967
  cosmetics,
663
- motion,
968
+ get motion() {
969
+ return motionFor(activeUserId());
970
+ },
971
+ motionFor,
664
972
  },
665
973
  item: {
666
974
  use: {
@@ -680,6 +988,11 @@ export function createGameContext(options) {
680
988
  input,
681
989
  subscribe: signal.subscribe,
682
990
  version: signal.version,
991
+ snapshot: () => composeWorldSnapshot(snapshotModules),
992
+ hydrate(snapshot) {
993
+ applyWorldSnapshot(snapshotModules, snapshot);
994
+ signal.notify();
995
+ },
683
996
  };
684
997
  return ctx;
685
998
  }