@jgengine/core 0.19.0 → 0.22.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 (266) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/ai/decisionGraph.d.ts +2 -0
  3. package/dist/ai/decisionGraph.js +6 -0
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/anim/animGraph.d.ts +33 -2
  7. package/dist/anim/animGraph.js +200 -12
  8. package/dist/anim/locomotionGraph.d.ts +11 -3
  9. package/dist/anim/locomotionGraph.js +25 -15
  10. package/dist/audio/audioFalloff.d.ts +4 -0
  11. package/dist/audio/voiceAllocator.d.ts +46 -0
  12. package/dist/audio/voiceAllocator.js +76 -0
  13. package/dist/combat/abilityKit.d.ts +19 -0
  14. package/dist/combat/abilityKit.js +40 -4
  15. package/dist/combat/death.d.ts +4 -3
  16. package/dist/combat/death.js +3 -3
  17. package/dist/combat/magazine.d.ts +9 -0
  18. package/dist/combat/magazine.js +24 -2
  19. package/dist/combat/projectiles.d.ts +94 -2
  20. package/dist/combat/projectiles.js +282 -61
  21. package/dist/combat/regenShield.d.ts +14 -0
  22. package/dist/combat/regenShield.js +23 -7
  23. package/dist/crafting/recipeCatalog.d.ts +16 -0
  24. package/dist/crafting/recipeCatalog.js +55 -0
  25. package/dist/economy/auctionBook.d.ts +1 -1
  26. package/dist/economy/auctionBook.js +11 -1
  27. package/dist/economy/listingBook.d.ts +1 -1
  28. package/dist/economy/listingBook.js +11 -1
  29. package/dist/economy/resourceLedger.js +3 -3
  30. package/dist/economy/shopStock.js +7 -6
  31. package/dist/economy/techTree.js +7 -6
  32. package/dist/economy/unlockPoints.js +3 -6
  33. package/dist/editor/commandApply.js +89 -16
  34. package/dist/editor/commands.d.ts +38 -1
  35. package/dist/editor/commands.js +320 -3
  36. package/dist/editor/creatorStorage.d.ts +59 -0
  37. package/dist/editor/creatorStorage.js +164 -0
  38. package/dist/editor/document.d.ts +4 -0
  39. package/dist/editor/document.js +116 -17
  40. package/dist/editor/index.d.ts +3 -0
  41. package/dist/editor/index.js +1 -0
  42. package/dist/editor/liveSync.d.ts +4 -1
  43. package/dist/editor/liveSync.js +26 -10
  44. package/dist/editor/materialAuthoring.d.ts +14 -0
  45. package/dist/editor/materialAuthoring.js +38 -0
  46. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  47. package/dist/editor/modelNodeVisibility.js +29 -0
  48. package/dist/editor/movementCatalog.d.ts +90 -0
  49. package/dist/editor/movementCatalog.js +144 -0
  50. package/dist/editor/simulation.d.ts +60 -0
  51. package/dist/editor/simulation.js +202 -0
  52. package/dist/editor/staticPrefab.d.ts +17 -0
  53. package/dist/editor/staticPrefab.js +51 -0
  54. package/dist/editor/terrainMaterials.d.ts +10 -0
  55. package/dist/editor/terrainMaterials.js +13 -0
  56. package/dist/editor/types.d.ts +11 -0
  57. package/dist/game/cameraConfig.d.ts +3 -1
  58. package/dist/game/clipRoles.d.ts +2 -1
  59. package/dist/game/clipRoles.js +7 -3
  60. package/dist/game/contentValidation.d.ts +69 -0
  61. package/dist/game/contentValidation.js +118 -0
  62. package/dist/game/controlGate.d.ts +8 -0
  63. package/dist/game/controlGate.js +43 -6
  64. package/dist/game/defineGame.d.ts +32 -3
  65. package/dist/game/dialogueGraph.d.ts +20 -0
  66. package/dist/game/dialogueGraph.js +107 -0
  67. package/dist/game/events.d.ts +5 -0
  68. package/dist/game/keyValueStore.d.ts +17 -4
  69. package/dist/game/keyValueStore.js +35 -23
  70. package/dist/game/lootModifiers.d.ts +17 -0
  71. package/dist/game/lootModifiers.js +26 -0
  72. package/dist/game/narrativeAuthoring.d.ts +67 -0
  73. package/dist/game/narrativeAuthoring.js +122 -0
  74. package/dist/game/playableGame.d.ts +52 -20
  75. package/dist/game/quest.d.ts +15 -0
  76. package/dist/game/quest.js +209 -63
  77. package/dist/game/questCatalog.d.ts +50 -0
  78. package/dist/game/questCatalog.js +205 -0
  79. package/dist/game/saveStore.d.ts +6 -5
  80. package/dist/game/saveStore.js +57 -57
  81. package/dist/game/systemRuntime.d.ts +2 -2
  82. package/dist/game/systemRuntime.js +84 -33
  83. package/dist/gameplay.d.ts +3 -0
  84. package/dist/gameplay.js +3 -0
  85. package/dist/interaction/promptRegistry.d.ts +5 -4
  86. package/dist/interaction/promptRegistry.js +8 -2
  87. package/dist/inventory/inventoryModel.js +9 -6
  88. package/dist/inventory/shapedGrid.d.ts +48 -9
  89. package/dist/inventory/shapedGrid.js +69 -10
  90. package/dist/item/durability.js +4 -0
  91. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  92. package/dist/item/itemInstanceRegistry.js +111 -7
  93. package/dist/item/itemgen.d.ts +1 -1
  94. package/dist/item/itemgen.js +1 -1
  95. package/dist/item/modularItem.js +7 -1
  96. package/dist/material/appearanceSignals.d.ts +53 -0
  97. package/dist/material/appearanceSignals.js +47 -0
  98. package/dist/material/hairCards.d.ts +47 -0
  99. package/dist/material/hairCards.js +136 -0
  100. package/dist/material/materialAsset.d.ts +162 -0
  101. package/dist/material/materialAsset.js +382 -0
  102. package/dist/meta/changelog.d.ts +1 -1
  103. package/dist/meta/changelog.js +348 -1
  104. package/dist/movement/characterController.d.ts +5 -2
  105. package/dist/movement/characterController.js +50 -23
  106. package/dist/movement/freeFlight.d.ts +2 -2
  107. package/dist/movement/freeFlight.js +3 -3
  108. package/dist/movement/movementModel.d.ts +24 -1
  109. package/dist/movement/movementModel.js +74 -7
  110. package/dist/movement/playerMovement.d.ts +28 -1
  111. package/dist/movement/playerMovement.js +425 -102
  112. package/dist/movement/poseState.d.ts +2 -0
  113. package/dist/movement/poseState.js +29 -1
  114. package/dist/movement/solidObstacles.d.ts +3 -1
  115. package/dist/movement/solidObstacles.js +8 -5
  116. package/dist/movement/terrainGrade.d.ts +13 -0
  117. package/dist/movement/terrainGrade.js +33 -0
  118. package/dist/movement/voxelController.d.ts +2 -2
  119. package/dist/movement/voxelController.js +3 -3
  120. package/dist/nav/navGrid.d.ts +19 -1
  121. package/dist/nav/navGrid.js +26 -10
  122. package/dist/nav/solidRoute.d.ts +50 -0
  123. package/dist/nav/solidRoute.js +144 -0
  124. package/dist/physics/ballisticSweep.d.ts +10 -3
  125. package/dist/physics/ballisticSweep.js +53 -29
  126. package/dist/physics/forceVolume.d.ts +41 -0
  127. package/dist/physics/forceVolume.js +88 -0
  128. package/dist/physics/kinematicVehicle.d.ts +8 -3
  129. package/dist/physics/kinematicVehicle.js +2 -2
  130. package/dist/physics/physicsWorldBackend.js +19 -3
  131. package/dist/physics/spatialGrid.d.ts +1 -1
  132. package/dist/physics/spatialGrid.js +8 -9
  133. package/dist/physics/vehicleDynamics.d.ts +3 -2
  134. package/dist/physics/vehicleDynamics.js +2 -2
  135. package/dist/physics/vehicleObstacles.d.ts +3 -2
  136. package/dist/physics/vehicleObstacles.js +29 -5
  137. package/dist/runtime/cameraDirector.d.ts +39 -2
  138. package/dist/runtime/cameraDirector.js +162 -0
  139. package/dist/runtime/commandDispatch.d.ts +14 -0
  140. package/dist/runtime/commandDispatch.js +19 -0
  141. package/dist/runtime/context/combat.d.ts +11 -4
  142. package/dist/runtime/context/combat.js +178 -11
  143. package/dist/runtime/context/deathLoot.d.ts +9 -8
  144. package/dist/runtime/context/deathLoot.js +16 -16
  145. package/dist/runtime/context/scene.d.ts +2 -0
  146. package/dist/runtime/context/scene.js +13 -21
  147. package/dist/runtime/descriptors/baseline.js +34 -3
  148. package/dist/runtime/descriptors/features.d.ts +2 -1
  149. package/dist/runtime/descriptors/features.js +18 -0
  150. package/dist/runtime/gameContext.js +79 -13
  151. package/dist/runtime/gameContextTypes.d.ts +14 -0
  152. package/dist/runtime/headlessRunner.d.ts +3 -1
  153. package/dist/runtime/headlessRunner.js +11 -11
  154. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  155. package/dist/runtime/hostedGameRunner.js +156 -10
  156. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  157. package/dist/runtime/hostedWorldSession.js +154 -16
  158. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  159. package/dist/runtime/inputRecorder.d.ts +1 -1
  160. package/dist/runtime/inputRecorder.js +21 -1
  161. package/dist/runtime/inputSnapshot.d.ts +16 -4
  162. package/dist/runtime/inputSnapshot.js +31 -3
  163. package/dist/runtime/runtimeSave.d.ts +10 -6
  164. package/dist/runtime/runtimeSave.js +11 -5
  165. package/dist/runtime/simContext.d.ts +8 -3
  166. package/dist/runtime/simContext.js +7 -3
  167. package/dist/runtime/simSnapshot.js +2 -0
  168. package/dist/runtime/worldReplication.d.ts +2 -0
  169. package/dist/runtime/worldReplication.js +1 -1
  170. package/dist/scene/assetCatalog.d.ts +2 -0
  171. package/dist/scene/behaviorRuntime.d.ts +26 -5
  172. package/dist/scene/behaviorRuntime.js +263 -16
  173. package/dist/scene/behaviors.d.ts +30 -1
  174. package/dist/scene/behaviors.js +29 -0
  175. package/dist/scene/entityStore.d.ts +14 -3
  176. package/dist/scene/entityStore.js +45 -9
  177. package/dist/scene/mount.d.ts +9 -0
  178. package/dist/scene/mount.js +35 -0
  179. package/dist/scene/possession.d.ts +2 -0
  180. package/dist/scene/possession.js +21 -1
  181. package/dist/scene/sceneRaycast.d.ts +7 -0
  182. package/dist/scene/sceneRaycast.js +55 -0
  183. package/dist/scene/spatial.d.ts +20 -1
  184. package/dist/scene/spatial.js +94 -2
  185. package/dist/scene/vehicleSeat.d.ts +36 -2
  186. package/dist/scene/vehicleSeat.js +89 -2
  187. package/dist/stats/accumulatorMeter.d.ts +10 -0
  188. package/dist/stats/accumulatorMeter.js +6 -0
  189. package/dist/stats/eventMeter.d.ts +9 -2
  190. package/dist/stats/eventMeter.js +5 -2
  191. package/dist/store/defineStore.d.ts +3 -1
  192. package/dist/store/defineStore.js +3 -1
  193. package/dist/store/observableKeyedStore.d.ts +1 -1
  194. package/dist/store/observableKeyedStore.js +8 -6
  195. package/dist/time/simClock.d.ts +6 -2
  196. package/dist/time/simClock.js +5 -0
  197. package/dist/vfx/particleDirector.d.ts +58 -6
  198. package/dist/vfx/particleDirector.js +113 -22
  199. package/dist/vfx/particles.d.ts +68 -5
  200. package/dist/vfx/particles.js +284 -32
  201. package/dist/visibility/assetStreaming.d.ts +21 -3
  202. package/dist/visibility/assetStreaming.js +108 -24
  203. package/dist/visibility/settings.d.ts +8 -2
  204. package/dist/visibility/settings.js +3 -0
  205. package/dist/visibility/visibilitySystem.js +1 -0
  206. package/dist/work/expedition.d.ts +110 -0
  207. package/dist/work/expedition.js +195 -0
  208. package/dist/work/staffedStation.d.ts +29 -0
  209. package/dist/work/staffedStation.js +27 -0
  210. package/dist/world/authoredAnimation.d.ts +38 -0
  211. package/dist/world/authoredAnimation.js +99 -0
  212. package/dist/world/authoredObjects.d.ts +24 -4
  213. package/dist/world/authoredObjects.js +43 -16
  214. package/dist/world/authoredSimulation.d.ts +74 -0
  215. package/dist/world/authoredSimulation.js +302 -0
  216. package/dist/world/authoredWeather.d.ts +120 -0
  217. package/dist/world/authoredWeather.js +314 -0
  218. package/dist/world/buildingKit.d.ts +8 -3
  219. package/dist/world/buildingKit.js +21 -8
  220. package/dist/world/environmentForces.d.ts +21 -0
  221. package/dist/world/environmentForces.js +42 -0
  222. package/dist/world/features.d.ts +3 -3
  223. package/dist/world/footprintGrid.d.ts +55 -0
  224. package/dist/world/footprintGrid.js +201 -0
  225. package/dist/world/grassKind.d.ts +10 -0
  226. package/dist/world/grassKind.js +22 -0
  227. package/dist/world/pathTerrain.d.ts +1 -1
  228. package/dist/world/terraform.d.ts +2 -2
  229. package/dist/world/terraform.js +46 -18
  230. package/dist/world/weather.d.ts +17 -0
  231. package/dist/world/weather.js +116 -20
  232. package/dist/world/worldSolids.d.ts +11 -0
  233. package/dist/world/worldSolids.js +112 -1
  234. package/dist/world.d.ts +5 -5
  235. package/dist/world.js +4 -4
  236. package/package.json +1 -1
  237. package/skills/ce-handoff/SKILL.md +1 -1
  238. package/skills/game-design/SKILL.md +2 -0
  239. package/skills/jgengine/SKILL.md +23 -2
  240. package/skills/jgengine/api.md +28 -17
  241. package/skills/jgengine/recipes/minimal-game.md +7 -2
  242. package/skills/jgengine-combat/SKILL.md +8 -2
  243. package/skills/jgengine-combat/api.md +22 -10
  244. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  245. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  246. package/skills/jgengine-combat/reference.md +24 -2
  247. package/skills/jgengine-gameplay/SKILL.md +63 -0
  248. package/skills/jgengine-gameplay/api.md +131 -37
  249. package/skills/jgengine-gameplay/capabilities.md +68 -0
  250. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  251. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  252. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  253. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  254. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  255. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  256. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  257. package/skills/jgengine-gameplay/reference-systems.md +119 -0
  258. package/skills/jgengine-world/SKILL.md +12 -3
  259. package/skills/jgengine-world/api.md +201 -58
  260. package/skills/jgengine-world/capabilities.md +116 -3
  261. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  262. package/skills/jgengine-world/recipes/controls.md +4 -0
  263. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  264. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  265. package/skills/jgengine-world/reference.md +167 -5
  266. package/skills/jgengine-world/visibility.md +28 -1
@@ -1,47 +1,45 @@
1
- import { defaultKeyValueStorage } from "./keyValueStore.js";
2
- function readSafe(storage, key) {
3
- if (storage === null)
4
- return null;
5
- try {
6
- return storage.getItem(key);
7
- }
8
- catch {
9
- return null;
10
- }
11
- }
1
+ import { defaultKeyValueStorage, reportStorageFailure } from "./keyValueStore.js";
12
2
  /**
13
3
  * A {@link SaveBackend} over a synchronous {@link KeyValueStorage} — the
14
4
  * browser's `localStorage` by default (offline, on-device saves), a test stub,
15
- * or `null` for memory-only. Storage errors (quota exceeded, private mode, no
16
- * DOM) degrade to no-ops, so a save never throws into a game tick.
5
+ * or `null` for a no-op backend. Default fallback ignores failures; `errorMode: "throw"`
6
+ * rejects failed operations and unavailable storage, so SaveStore status/onError can report them.
7
+ * Use {@link memorySaveBackend} for deliberate ephemeral persistence.
17
8
  *
18
9
  * @capability local-save persist a game to on-device localStorage (offline)
19
10
  */
20
- export function localSaveBackend(storage) {
21
- const store = storage === undefined ? defaultKeyValueStorage() : storage;
11
+ export function localSaveBackend(storage, policy = {}) {
12
+ const resolve = () => {
13
+ const store = storage === undefined ? defaultKeyValueStorage((error) => { throw error; }) : storage;
14
+ if (store === null)
15
+ throw new Error("Local storage is unavailable");
16
+ return store;
17
+ };
22
18
  return {
23
- read: (key) => Promise.resolve(readSafe(store, key)),
24
- write: (key, value) => {
25
- if (store !== null) {
26
- try {
27
- store.setItem(key, value);
28
- }
29
- catch {
30
- // storage rejected the write; the store keeps the value in memory
31
- }
19
+ async read(key) {
20
+ try {
21
+ return resolve().getItem(key);
22
+ }
23
+ catch (error) {
24
+ reportStorageFailure(policy, { operation: "read", key, error });
25
+ return null;
32
26
  }
33
- return Promise.resolve();
34
27
  },
35
- remove: (key) => {
36
- if (store !== null) {
37
- try {
38
- store.removeItem(key);
39
- }
40
- catch {
41
- // ignore removal failure
42
- }
28
+ async write(key, value) {
29
+ try {
30
+ resolve().setItem(key, value);
31
+ }
32
+ catch (error) {
33
+ reportStorageFailure(policy, { operation: "write", key, error });
34
+ }
35
+ },
36
+ async remove(key) {
37
+ try {
38
+ resolve().removeItem(key);
39
+ }
40
+ catch (error) {
41
+ reportStorageFailure(policy, { operation: "remove", key, error });
43
42
  }
44
- return Promise.resolve();
45
43
  },
46
44
  };
47
45
  }
@@ -143,10 +141,10 @@ export function createSaveStore(config) {
143
141
  return parsed;
144
142
  }
145
143
  async function readSlots() {
144
+ const raw = await backend.read(indexKey());
145
+ if (raw === null)
146
+ return [];
146
147
  try {
147
- const raw = await backend.read(indexKey());
148
- if (raw === null)
149
- return [];
150
148
  const parsed = JSON.parse(raw);
151
149
  return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
152
150
  }
@@ -155,26 +153,16 @@ export function createSaveStore(config) {
155
153
  }
156
154
  }
157
155
  async function rememberSlot(slot) {
158
- try {
159
- const list = await readSlots();
160
- if (list.includes(slot))
161
- return;
162
- await backend.write(indexKey(), JSON.stringify([...list, slot]));
163
- }
164
- catch (error) {
165
- reportError(error);
166
- }
156
+ const list = await readSlots();
157
+ if (list.includes(slot))
158
+ return;
159
+ await backend.write(indexKey(), JSON.stringify([...list, slot]));
167
160
  }
168
161
  async function forgetSlot(slot) {
169
- try {
170
- const list = await readSlots();
171
- if (!list.includes(slot))
172
- return;
173
- await backend.write(indexKey(), JSON.stringify(list.filter((entry) => entry !== slot)));
174
- }
175
- catch (error) {
176
- reportError(error);
177
- }
162
+ const list = await readSlots();
163
+ if (!list.includes(slot))
164
+ return;
165
+ await backend.write(indexKey(), JSON.stringify(list.filter((entry) => entry !== slot)));
178
166
  }
179
167
  function clearTimer() {
180
168
  if (pendingTimer !== null) {
@@ -243,7 +231,10 @@ export function createSaveStore(config) {
243
231
  firstDirtyAt = null;
244
232
  setStatus("idle");
245
233
  emit();
246
- void rememberSlot(currentSlot);
234
+ void rememberSlot(currentSlot).catch((error) => {
235
+ setStatus("error");
236
+ reportError(error);
237
+ });
247
238
  return current;
248
239
  }
249
240
  catch (error) {
@@ -298,7 +289,16 @@ export function createSaveStore(config) {
298
289
  firstDirtyAt = null;
299
290
  return load();
300
291
  },
301
- slots: readSlots,
292
+ async slots() {
293
+ try {
294
+ return await readSlots();
295
+ }
296
+ catch (error) {
297
+ setStatus("error");
298
+ reportError(error);
299
+ return [];
300
+ }
301
+ },
302
302
  subscribe(listener) {
303
303
  listeners.add(listener);
304
304
  return () => listeners.delete(listener);
@@ -15,14 +15,14 @@ export interface InstalledSystems {
15
15
  tick(ctx: GameContext, dt: number): void;
16
16
  /** Invoke every system's `reset` (scenario/run wipe). */
17
17
  reset(ctx: GameContext): void;
18
- /** Invoke every system's `dispose` in reverse install order. */
18
+ /** Retire once in reverse order; finish cleanup before rethrowing the first error. */
19
19
  dispose(ctx: GameContext): void;
20
20
  /** Run one manual system by id (no-op if missing or not manual). */
21
21
  runManual(ctx: GameContext, id: string, dt?: number): void;
22
22
  }
23
23
  /**
24
24
  * Install systems on a live context: `create` → bind events → register save/replicate → `start`.
25
- * Call once per world boot (from the composed loop's `onInit`).
25
+ * Call once per world boot. Failed installation retires entered systems and listeners, then rethrows the startup error.
26
26
  * @internal
27
27
  */
28
28
  export declare function installSystems(ctx: GameContext, systems: readonly SystemDefinition[], options?: CompileSystemScheduleOptions & {
@@ -7,7 +7,7 @@ function resolveModule(spec, ctx) {
7
7
  }
8
8
  /**
9
9
  * Install systems on a live context: `create` → bind events → register save/replicate → `start`.
10
- * Call once per world boot (from the composed loop's `onInit`).
10
+ * Call once per world boot. Failed installation retires entered systems and listeners, then rethrows the startup error.
11
11
  * @internal
12
12
  */
13
13
  export function installSystems(ctx, systems, options) {
@@ -16,29 +16,41 @@ export function installSystems(ctx, systems, options) {
16
16
  const unsubs = [];
17
17
  const intervalAcc = new Map();
18
18
  const fixedAcc = new Map();
19
- for (const system of list) {
20
- system.create?.(ctx);
21
- }
22
- for (const system of list) {
23
- if (system.events !== undefined) {
24
- for (const [name, handler] of Object.entries(system.events)) {
25
- if (handler === undefined)
26
- continue;
27
- const off = ctx.game.events.on(name, ((event) => {
28
- handler(ctx, event);
29
- }));
30
- unsubs.push(off);
19
+ const acquired = [];
20
+ let disposed = false;
21
+ try {
22
+ for (const system of list) {
23
+ acquired.push(system);
24
+ system.create?.(ctx);
25
+ }
26
+ for (const system of list) {
27
+ if (system.events !== undefined) {
28
+ for (const [name, handler] of Object.entries(system.events)) {
29
+ if (handler === undefined)
30
+ continue;
31
+ const off = ctx.game.events.on(name, ((event) => {
32
+ handler(ctx, event);
33
+ }));
34
+ unsubs.push(off);
35
+ }
31
36
  }
37
+ const save = resolveModule(system.save, ctx);
38
+ if (save !== undefined)
39
+ options?.modules?.registerSave(save);
40
+ const replicate = resolveModule(system.replicate, ctx);
41
+ if (replicate !== undefined)
42
+ options?.modules?.registerReplicate(replicate);
43
+ }
44
+ for (const system of list) {
45
+ system.start?.(ctx);
32
46
  }
33
- const save = resolveModule(system.save, ctx);
34
- if (save !== undefined)
35
- options?.modules?.registerSave(save);
36
- const replicate = resolveModule(system.replicate, ctx);
37
- if (replicate !== undefined)
38
- options?.modules?.registerReplicate(replicate);
39
47
  }
40
- for (const system of list) {
41
- system.start?.(ctx);
48
+ catch (error) {
49
+ try {
50
+ dispose(ctx);
51
+ }
52
+ catch { /* The startup error remains the first failure. */ }
53
+ throw error;
42
54
  }
43
55
  for (const group of schedule.fixed) {
44
56
  fixedAcc.set(group.rate, 0);
@@ -52,7 +64,7 @@ export function installSystems(ctx, systems, options) {
52
64
  }
53
65
  }
54
66
  function tick(ctx, dt) {
55
- if (!(dt > 0))
67
+ if (disposed || !(dt > 0))
56
68
  return;
57
69
  for (const group of schedule.fixed) {
58
70
  const step = 1 / group.rate;
@@ -79,6 +91,8 @@ export function installSystems(ctx, systems, options) {
79
91
  }
80
92
  }
81
93
  function reset(ctx) {
94
+ if (disposed)
95
+ return;
82
96
  for (const system of list)
83
97
  system.reset?.(ctx);
84
98
  for (const group of schedule.fixed)
@@ -87,14 +101,33 @@ export function installSystems(ctx, systems, options) {
87
101
  intervalAcc.set(interval.id, 0);
88
102
  }
89
103
  function dispose(ctx) {
90
- for (const off of unsubs)
91
- off();
104
+ if (disposed)
105
+ return;
106
+ disposed = true;
107
+ let failure;
108
+ for (const off of unsubs) {
109
+ try {
110
+ off();
111
+ }
112
+ catch (error) {
113
+ failure ??= { error };
114
+ }
115
+ }
92
116
  unsubs.length = 0;
93
- for (let i = list.length - 1; i >= 0; i -= 1) {
94
- list[i]?.dispose?.(ctx);
117
+ for (let i = acquired.length - 1; i >= 0; i -= 1) {
118
+ try {
119
+ acquired[i]?.dispose?.(ctx);
120
+ }
121
+ catch (error) {
122
+ failure ??= { error };
123
+ }
95
124
  }
125
+ if (failure !== undefined)
126
+ throw failure.error;
96
127
  }
97
128
  function runManual(ctx, id, dt = 0) {
129
+ if (disposed)
130
+ return;
98
131
  if (!schedule.manual.includes(id))
99
132
  return;
100
133
  schedule.systemsById.get(id)?.update?.(ctx, dt);
@@ -119,17 +152,18 @@ export function composeGameLoop(systems, loop, options) {
119
152
  if (systems === undefined || systems.length === 0) {
120
153
  return loop ?? {};
121
154
  }
122
- let installed;
155
+ const retired = new WeakSet();
123
156
  const loopBefore = options?.loopBeforeSystems === true;
124
157
  return {
125
158
  onInit(ctx) {
159
+ retired.delete(ctx);
126
160
  const modules = ctx.game.registerSave !== undefined && ctx.game.registerReplicate !== undefined
127
161
  ? {
128
162
  registerSave: (m) => ctx.game.registerSave(m),
129
163
  registerReplicate: (m) => ctx.game.registerReplicate(m),
130
164
  }
131
165
  : undefined;
132
- installed = installSystems(ctx, systems, { ...options, modules });
166
+ const installed = installSystems(ctx, systems, { ...options, modules });
133
167
  installedByCtx.set(ctx, installed);
134
168
  loop?.onInit?.(ctx);
135
169
  },
@@ -139,10 +173,10 @@ export function composeGameLoop(systems, loop, options) {
139
173
  onTick(ctx, dt) {
140
174
  if (loopBefore) {
141
175
  loop?.onTick?.(ctx, dt);
142
- installed?.tick(ctx, dt);
176
+ installedByCtx.get(ctx)?.tick(ctx, dt);
143
177
  }
144
178
  else {
145
- installed?.tick(ctx, dt);
179
+ installedByCtx.get(ctx)?.tick(ctx, dt);
146
180
  loop?.onTick?.(ctx, dt);
147
181
  }
148
182
  },
@@ -150,13 +184,30 @@ export function composeGameLoop(systems, loop, options) {
150
184
  loop?.onPlayerLeave?.(ctx, player);
151
185
  },
152
186
  onReset(ctx) {
153
- installed?.reset(ctx);
187
+ installedByCtx.get(ctx)?.reset(ctx);
154
188
  loop?.onReset?.(ctx);
155
189
  },
156
190
  onDispose(ctx) {
157
- installed?.dispose(ctx);
191
+ if (retired.has(ctx))
192
+ return;
193
+ retired.add(ctx);
194
+ const installed = installedByCtx.get(ctx);
158
195
  installedByCtx.delete(ctx);
159
- loop?.onDispose?.(ctx);
196
+ let failure;
197
+ try {
198
+ installed?.dispose(ctx);
199
+ }
200
+ catch (error) {
201
+ failure = { error };
202
+ }
203
+ try {
204
+ loop?.onDispose?.(ctx);
205
+ }
206
+ catch (error) {
207
+ failure ??= { error };
208
+ }
209
+ if (failure !== undefined)
210
+ throw failure.error;
160
211
  },
161
212
  };
162
213
  }
@@ -31,6 +31,7 @@ export { medalFor, rankLeaderboard, type RankLeaderboardOptions, type RankableRo
31
31
  export { createLevelSequence, type LevelSequence } from "./game/levelSequence.js";
32
32
  export { createLoadouts, type LoadoutDef } from "./game/loadout.js";
33
33
  export { evaluateLootFilter, lootFilter, type LootFilterRule } from "./game/lootFilter.js";
34
+ export { timeScaledRarity, type TimeScaledRarityOptions } from "./game/lootModifiers.js";
34
35
  export { createLootPipeline, defineLootPipeline, type LootDropProvenance, type LootModifier, type LootPipeline, type LootPipelineDef, type LootPipelineDeps, type LootPlanEntry, type LootResolution, type LootResolveContext, type LootRollPlan, type LootStage, type LootStageKind, type LootStageStatus, type LootStageTrace, } from "./game/lootPipeline.js";
35
36
  export { createLootRegistry, lootTable, type Drop, type LootTableDef } from "./game/lootTable.js";
36
37
  export { resolveOneShotClip } from "./game/modelAnimation.js";
@@ -100,5 +101,7 @@ export { createMultiRegionHealth, type MultiRegionHealth } from "./survival/regi
100
101
  export { createCommitController } from "./turn/commit.js";
101
102
  export { createIntentBoard } from "./turn/intent.js";
102
103
  export { createTurnLoop, type TurnLoop } from "./turn/turnLoop.js";
104
+ export { dispatchExpedition, recallExpedition, returnExpeditionNow, settleExpedition, type ExpeditionConfig, type ExpeditionDispatch, type ExpeditionEvent, type ExpeditionHazard, type ExpeditionState, type ExpeditionSupplyRule, } from "./work/expedition.js";
103
105
  export { activeJobs, cancelJob, createWorkQueue, enqueue, fifoOrdering, jobById, jobProgress, pauseJob, priorityOrdering, queueSize, queuedJobs, resumeJob, tick, type CancelResult, type EnqueueOptions, type EnqueueResult, type Job, type JobId, type JobOrdering, type JobStatus, type JobValidation, type TickResult, type WorkQueueConfig, type WorkQueueEvent, type WorkQueueState, } from "./work/jobQueue.js";
106
+ export { stationOutputRate, type StaffedStation, type StationRateTuning, type StationWorker, } from "./work/staffedStation.js";
104
107
  export { unitTrainingConfig, type ResourceCost, type TrainableUnitDef, type UnitReservation, type UnitSpawnOrder, type UnitTrainingOptions, type UnitTrainingSpec, } from "./work/unitTraining.js";
package/dist/gameplay.js CHANGED
@@ -31,6 +31,7 @@ export { medalFor, rankLeaderboard, } from "./game/leaderboardRank.js";
31
31
  export { createLevelSequence } from "./game/levelSequence.js";
32
32
  export { createLoadouts } from "./game/loadout.js";
33
33
  export { evaluateLootFilter, lootFilter } from "./game/lootFilter.js";
34
+ export { timeScaledRarity } from "./game/lootModifiers.js";
34
35
  export { createLootPipeline, defineLootPipeline, } from "./game/lootPipeline.js";
35
36
  export { createLootRegistry, lootTable } from "./game/lootTable.js";
36
37
  export { resolveOneShotClip } from "./game/modelAnimation.js";
@@ -100,5 +101,7 @@ export { createMultiRegionHealth } from "./survival/regionHealth.js";
100
101
  export { createCommitController } from "./turn/commit.js";
101
102
  export { createIntentBoard } from "./turn/intent.js";
102
103
  export { createTurnLoop } from "./turn/turnLoop.js";
104
+ export { dispatchExpedition, recallExpedition, returnExpeditionNow, settleExpedition, } from "./work/expedition.js";
103
105
  export { activeJobs, cancelJob, createWorkQueue, enqueue, fifoOrdering, jobById, jobProgress, pauseJob, priorityOrdering, queueSize, queuedJobs, resumeJob, tick, } from "./work/jobQueue.js";
106
+ export { stationOutputRate, } from "./work/staffedStation.js";
104
107
  export { unitTrainingConfig, } from "./work/unitTraining.js";
@@ -29,13 +29,14 @@ export interface PromptRegistrySnapshot {
29
29
  export interface PromptRegistry {
30
30
  /**
31
31
  * Add (or replace, by id) a positioned prompt. Replacing an existing id keeps
32
- * its registration slot so tie-break order is stable. Does not re-resolve on
33
- * its own — the next {@link resolve} picks up the change.
32
+ * its registration slot so tie-break order is stable. Changed active content
33
+ * notifies immediately; selection is recalculated by the next {@link resolve}.
34
34
  */
35
35
  register(prompt: PositionedPrompt): void;
36
36
  /**
37
37
  * Patch a registered prompt in place. No-op if `id` is unknown. Only the fields
38
- * present in `patch` change; the rest are preserved.
38
+ * present in `patch` change; the rest are preserved. Changed active content
39
+ * notifies immediately without recalculating selection.
39
40
  */
40
41
  update(id: string, patch: PositionedPromptPatch): void;
41
42
  /** Remove the prompt with this id, if present. Returns whether one was removed. */
@@ -54,7 +55,7 @@ export interface PromptRegistry {
54
55
  resolve(playerPosition: PromptPoint): PositionedPrompt | null;
55
56
  /** The prompt selected by the most recent {@link resolve}, or `null`. */
56
57
  active(): PositionedPrompt | null;
57
- /** Observe active-prompt changes (resolve transitions, clear, restore). Returns an unsubscribe fn. */
58
+ /** Observe active-prompt content changes, selection transitions, clear and restore. Returns an unsubscribe fn. */
58
59
  subscribe(listener: () => void): () => void;
59
60
  /** Serializable state for a save. */
60
61
  snapshot(): PromptRegistrySnapshot;
@@ -1,4 +1,4 @@
1
- import { resolveActivePrompt, } from "./proximityPrompt.js";
1
+ import { positionedPromptsEqual, resolveActivePrompt, } from "./proximityPrompt.js";
2
2
  /** Structural copy of a positioned prompt so external mutation can't corrupt registry state. */
3
3
  function clonePrompt(prompt) {
4
4
  const copy = {
@@ -50,8 +50,12 @@ export function createPromptRegistry() {
50
50
  }
51
51
  return {
52
52
  register(prompt) {
53
- prompts.set(prompt.id, clonePrompt(prompt));
53
+ const existing = prompts.get(prompt.id);
54
+ const next = clonePrompt(prompt);
55
+ prompts.set(prompt.id, next);
54
56
  invalidate();
57
+ if (activeId === prompt.id && existing !== undefined && !positionedPromptsEqual(existing, next))
58
+ notify();
55
59
  },
56
60
  update(id, patch) {
57
61
  const existing = prompts.get(id);
@@ -71,6 +75,8 @@ export function createPromptRegistry() {
71
75
  }
72
76
  prompts.set(id, merged);
73
77
  invalidate();
78
+ if (activeId === id && !positionedPromptsEqual(existing, merged))
79
+ notify();
74
80
  },
75
81
  unregister(id) {
76
82
  const removed = prompts.delete(id);
@@ -14,6 +14,9 @@ function violatesKind(layout, traits, itemId) {
14
14
  return kind === null || !layout.accepts.includes(kind);
15
15
  return kind !== layout.accepts;
16
16
  }
17
+ function validSlot(state, slot) {
18
+ return Number.isInteger(slot) && slot >= 0 && slot < state.slots.length;
19
+ }
17
20
  function computeCapacity(state, traits, itemId) {
18
21
  const limit = traits.stackLimit(itemId);
19
22
  let existingRoom = 0;
@@ -58,7 +61,7 @@ function autoPut(state, layout, traits, itemId, count) {
58
61
  return { status: "ok", state: { slots } };
59
62
  }
60
63
  function explicitPut(state, layout, traits, itemId, count, slot) {
61
- if (slot < 0 || slot >= state.slots.length)
64
+ if (!validSlot(state, slot))
62
65
  return { status: "rejected", reason: "invalid-slot" };
63
66
  if (violatesKind(layout, traits, itemId))
64
67
  return { status: "rejected", reason: "wrong-kind" };
@@ -120,7 +123,7 @@ export function hasItem(state, itemId, count) {
120
123
  }
121
124
  /** Move the stack at `fromSlot` to `toSlot` (or auto-stack when omitted) — swapping, merging with remainder, or relocating across two inventories. Rejects on empty source, wrong kind, or no space. */
122
125
  export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
123
- if (fromSlot < 0 || fromSlot >= from.slots.length)
126
+ if (!validSlot(from, fromSlot))
124
127
  return { status: "rejected", reason: "invalid-slot" };
125
128
  const source = from.slots[fromSlot];
126
129
  if (source === null)
@@ -138,7 +141,7 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
138
141
  const toSlots = sameInventory ? fromSlots : to.slots.slice();
139
142
  const limit = traits.stackLimit(source.itemId);
140
143
  if (toSlot !== undefined) {
141
- if (toSlot < 0 || toSlot >= toSlots.length)
144
+ if (!validSlot(to, toSlot))
142
145
  return { status: "rejected", reason: "invalid-slot" };
143
146
  const dest = toSlots[toSlot];
144
147
  if (dest === null) {
@@ -179,12 +182,12 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
179
182
  * @capability inventory-split split a stack into a target or first-empty slot
180
183
  */
181
184
  export function splitStack(state, slot, amount, toSlot) {
182
- if (slot < 0 || slot >= state.slots.length)
185
+ if (!validSlot(state, slot))
183
186
  return { status: "rejected", reason: "invalid-slot" };
184
187
  const source = state.slots[slot];
185
188
  if (source === null)
186
189
  return { status: "rejected", reason: "empty-slot" };
187
- if (amount <= 0 || amount >= source.count)
190
+ if (!Number.isFinite(amount) || amount <= 0 || amount >= source.count)
188
191
  return { status: "rejected", reason: "invalid-amount" };
189
192
  let target = toSlot;
190
193
  if (target === undefined) {
@@ -193,7 +196,7 @@ export function splitStack(state, slot, amount, toSlot) {
193
196
  return { status: "rejected", reason: "no-space" };
194
197
  }
195
198
  else {
196
- if (target < 0 || target >= state.slots.length)
199
+ if (!validSlot(state, target))
197
200
  return { status: "rejected", reason: "invalid-slot" };
198
201
  if (target === slot)
199
202
  return { status: "rejected", reason: "slot-occupied" };
@@ -18,7 +18,7 @@ export interface ShapedGrid<T> {
18
18
  readonly height: number;
19
19
  readonly placements: readonly Placement<T>[];
20
20
  }
21
- export type ShapedRejection = "out-of-bounds" | "overlap" | "duplicate-id" | "unknown-id";
21
+ export type ShapedRejection = "out-of-bounds" | "overlap" | "duplicate-id" | "unknown-id" | "invalid-footprint" | "invalid-origin";
22
22
  export interface ShapedPlaceResult<T> {
23
23
  status: "ok";
24
24
  grid: ShapedGrid<T>;
@@ -36,19 +36,58 @@ export declare function rotateFootprint(footprint: Footprint, rotation: Rotation
36
36
  /** @internal */
37
37
  export declare function occupiedCells(footprint: Footprint, origin: Cell, rotation: Rotation): Cell[];
38
38
  /**
39
- * A spatial grid inventory that holds shaped multi-cell items, Resident-Evil/Tarkov style.
39
+ * An immutable spatial inventory board holding caller-defined multi-cell items.
40
40
  *
41
- * @capability tetris-inventory a spatial grid inventory holding shaped multi-cell items
42
- * @internal
43
- */
41
+ * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
42
+ */
44
43
  export declare function createShapedGrid<T>(width: number, height: number): ShapedGrid<T>;
45
- /** @internal */
44
+ /**
45
+ * Check bounds and overlap for a discrete footprint, optionally ignoring the item being moved.
46
+ * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
47
+ */
46
48
  export declare function canPlace<T>(grid: ShapedGrid<T>, footprint: Footprint, origin: Cell, rotation: Rotation, ignoreId?: string): ShapedRejection | null;
47
- /** @internal */
49
+ /** Search policy: rotation order first, then rows and columns; the work budget counts candidate origins. */
50
+ export interface ShapedPlacementSearchOptions {
51
+ /** Defaults to `[0, 1, 2, 3]`; a game can prefer or forbid rotations. */
52
+ rotations?: readonly Rotation[];
53
+ /** Ignore an existing placement while searching for its next position. */
54
+ ignoreId?: string;
55
+ /** Nonnegative integer candidate limit; defaults to 4096. */
56
+ maxChecks?: number;
57
+ }
58
+ /** A detached placement, a completed no-fit search, or an incomplete search that reached its budget. */
59
+ export type ShapedPlacementSearchResult = {
60
+ status: "found";
61
+ origin: Cell;
62
+ rotation: Rotation;
63
+ checks: number;
64
+ } | {
65
+ status: "no-space";
66
+ checks: number;
67
+ } | {
68
+ status: "budget-exceeded";
69
+ checks: number;
70
+ };
71
+ /**
72
+ * Find the first legal placement without mutating the board; occupancy is indexed once per search.
73
+ *
74
+ * @capability shaped-placement find a shaped inventory item's first fit with ordered rotations, row-major placement, and an explicit search budget
75
+ */
76
+ export declare function findShapedPlacement<T>(grid: ShapedGrid<T>, footprint: Footprint, options?: ShapedPlacementSearchOptions): ShapedPlacementSearchResult;
77
+ /**
78
+ * Place a caller-defined item without mutating the source board; duplicate ids reject.
79
+ * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
80
+ */
48
81
  export declare function placeShaped<T>(grid: ShapedGrid<T>, item: ShapedItem<T>, origin: Cell, rotation?: Rotation): ShapedResult<T>;
49
- /** @internal */
82
+ /**
83
+ * Remove one placement without mutating the source board.
84
+ * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
85
+ */
50
86
  export declare function removeShaped<T>(grid: ShapedGrid<T>, id: string): ShapedResult<T>;
51
- /** @internal */
87
+ /**
88
+ * Move or rotate one placement, ignoring its current cells during overlap checks.
89
+ * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
90
+ */
52
91
  export declare function moveShaped<T>(grid: ShapedGrid<T>, id: string, origin: Cell, rotation?: Rotation): ShapedResult<T>;
53
92
  /** @internal */
54
93
  export declare function cellOccupant<T>(grid: ShapedGrid<T>, cell: Cell): string | null;