@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
@@ -0,0 +1,69 @@
1
+ /** A definition in a game-owned namespace, located in its authored document. */
2
+ export interface ContentEntry {
3
+ kind: string;
4
+ id: string;
5
+ path: string;
6
+ }
7
+ /** A located reference; namespaces can express game-owned roles such as questgiver. */
8
+ export interface ContentReference extends ContentEntry {
9
+ contentId: string;
10
+ /** Optional game-declared relationship, such as the quests offered by this particular giver. */
11
+ allowedIds?: readonly string[];
12
+ }
13
+ /** A repairable authoring issue. Paths are caller document locations, usually JSON Pointers. */
14
+ export interface ContentIssue {
15
+ code: string;
16
+ severity: "error" | "warning";
17
+ path: string;
18
+ contentId?: string;
19
+ referenceId?: string;
20
+ message: string;
21
+ repair: string;
22
+ }
23
+ /**
24
+ * Index definitions once and check references in input order, without mutating the catalog.
25
+ * Namespace strings and roles are game-owned; identical ids in different namespaces are valid.
26
+ * Typed input must be decoded before calling. Work is O(definitions + references + allowed ids).
27
+ *
28
+ * @capability content-reference-validation locate duplicate content ids and broken references across game-owned catalog namespaces with repair guidance
29
+ */
30
+ export declare function validateContentReferences(entries: readonly ContentEntry[], references: readonly ContentReference[]): ContentIssue[];
31
+ /** A possible source or action; all required facts must be reachable to provide its facts. */
32
+ export interface ContentProgressionRule {
33
+ id: string;
34
+ path: string;
35
+ requires: readonly string[];
36
+ provides: readonly string[];
37
+ }
38
+ /** A game-declared availability requirement, located at the content that needs it. */
39
+ export interface ContentProgressionRequirement {
40
+ id: string;
41
+ path: string;
42
+ contentId: string;
43
+ }
44
+ /** Explicit possible sources; opaque fact strings may represent items, unlocks or skill milestones. */
45
+ export interface ContentProgressionInput {
46
+ initial: readonly string[];
47
+ rules: readonly ContentProgressionRule[];
48
+ required?: readonly ContentProgressionRequirement[];
49
+ /** True only when the game declares all possible sources; otherwise blocked routes are warnings. */
50
+ closedWorld?: boolean;
51
+ }
52
+ /** Potential availability and diagnostics in deterministic input/traversal order. */
53
+ export interface ContentProgressionResult {
54
+ reachable: string[];
55
+ reachableRules: string[];
56
+ issues: ContentIssue[];
57
+ }
58
+ /**
59
+ * Check declared potential progression using a dependency queue in O(facts + rules + edges).
60
+ * Alternatives are separate rules; every requirement within one rule is necessary.
61
+ * Seeded cycles are valid. An unseeded cycle is blocked only under the declared sources.
62
+ * Facts describe possible availability, not quantities, consumption, mutually exclusive choices,
63
+ * chronology, station proximity or economic balance. Encode those constraints in game-owned rules
64
+ * or playtests. Skill training caps must be explicitly translated into attainable milestones.
65
+ * This is offline authoring analysis, not a quest engine or a per-frame catalog scan.
66
+ *
67
+ * @capability content-progression-validation diagnose unavailable sources and blocked cross-content progression under explicit game-owned rules without rejecting intentional seeded cycles
68
+ */
69
+ export declare function validateContentProgression(input: ContentProgressionInput): ContentProgressionResult;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Index definitions once and check references in input order, without mutating the catalog.
3
+ * Namespace strings and roles are game-owned; identical ids in different namespaces are valid.
4
+ * Typed input must be decoded before calling. Work is O(definitions + references + allowed ids).
5
+ *
6
+ * @capability content-reference-validation locate duplicate content ids and broken references across game-owned catalog namespaces with repair guidance
7
+ */
8
+ export function validateContentReferences(entries, references) {
9
+ const namespaces = new Map();
10
+ const issues = [];
11
+ for (const entry of entries) {
12
+ let ids = namespaces.get(entry.kind);
13
+ if (ids === undefined)
14
+ namespaces.set(entry.kind, ids = new Map());
15
+ const earlier = ids.get(entry.id);
16
+ if (earlier !== undefined) {
17
+ issues.push({ code: "duplicate-content", severity: "error", path: entry.path,
18
+ contentId: entry.id, referenceId: entry.id,
19
+ message: `Duplicate ${entry.kind} "${entry.id}"; first defined at ${earlier}.`,
20
+ repair: "Give the definitions distinct ids and update their references, or remove the duplicate." });
21
+ }
22
+ else
23
+ ids.set(entry.id, entry.path);
24
+ }
25
+ for (const reference of references) {
26
+ if (!namespaces.get(reference.kind)?.has(reference.id)) {
27
+ issues.push({ code: "unknown-reference", severity: "error", path: reference.path,
28
+ contentId: reference.contentId, referenceId: reference.id,
29
+ message: `Content "${reference.contentId}" references unknown ${reference.kind} "${reference.id}".`,
30
+ repair: `Correct this reference or declare "${reference.id}" in the ${reference.kind} catalog.` });
31
+ }
32
+ else if (reference.allowedIds !== undefined && !reference.allowedIds.includes(reference.id)) {
33
+ issues.push({ code: "inconsistent-reference", severity: "error", path: reference.path,
34
+ contentId: reference.contentId, referenceId: reference.id,
35
+ message: `Content "${reference.contentId}" references ${reference.kind} "${reference.id}" outside its declared relationship.`,
36
+ repair: `Use an allowed id (${reference.allowedIds.map((id) => `"${id}"`).join(", ") || "none declared"}) or correct the game-owned relationship declaration.` });
37
+ }
38
+ }
39
+ return issues;
40
+ }
41
+ /**
42
+ * Check declared potential progression using a dependency queue in O(facts + rules + edges).
43
+ * Alternatives are separate rules; every requirement within one rule is necessary.
44
+ * Seeded cycles are valid. An unseeded cycle is blocked only under the declared sources.
45
+ * Facts describe possible availability, not quantities, consumption, mutually exclusive choices,
46
+ * chronology, station proximity or economic balance. Encode those constraints in game-owned rules
47
+ * or playtests. Skill training caps must be explicitly translated into attainable milestones.
48
+ * This is offline authoring analysis, not a quest engine or a per-frame catalog scan.
49
+ *
50
+ * @capability content-progression-validation diagnose unavailable sources and blocked cross-content progression under explicit game-owned rules without rejecting intentional seeded cycles
51
+ */
52
+ export function validateContentProgression(input) {
53
+ const issues = [];
54
+ const ids = new Set();
55
+ const available = new Set(input.initial);
56
+ const dependents = new Map();
57
+ const remaining = [];
58
+ const ready = [];
59
+ const reached = new Set();
60
+ for (let index = 0; index < input.rules.length; index++) {
61
+ const rule = input.rules[index];
62
+ if (ids.has(rule.id)) {
63
+ issues.push({ code: "duplicate-rule", severity: "error", path: rule.path,
64
+ contentId: rule.id, referenceId: rule.id, message: `Progression rule "${rule.id}" repeats.`,
65
+ repair: "Give each source/action a distinct rule id, or remove the duplicate." });
66
+ }
67
+ ids.add(rule.id);
68
+ let count = 0;
69
+ for (const fact of new Set(rule.requires)) {
70
+ if (available.has(fact))
71
+ continue;
72
+ count++;
73
+ let waiting = dependents.get(fact);
74
+ if (waiting === undefined)
75
+ dependents.set(fact, waiting = []);
76
+ waiting.push(index);
77
+ }
78
+ remaining.push(count);
79
+ if (count === 0)
80
+ ready.push(index);
81
+ }
82
+ const reachableRules = [];
83
+ for (let head = 0; head < ready.length; head++) {
84
+ const index = ready[head];
85
+ reached.add(index);
86
+ const rule = input.rules[index];
87
+ reachableRules.push(rule.id);
88
+ for (const fact of rule.provides) {
89
+ if (available.has(fact))
90
+ continue;
91
+ available.add(fact);
92
+ for (const dependent of dependents.get(fact) ?? []) {
93
+ remaining[dependent]--;
94
+ if (remaining[dependent] === 0)
95
+ ready.push(dependent);
96
+ }
97
+ }
98
+ }
99
+ const severity = input.closedWorld === true ? "error" : "warning";
100
+ for (let index = 0; index < input.rules.length; index++) {
101
+ if (reached.has(index))
102
+ continue;
103
+ const rule = input.rules[index];
104
+ const missing = [...new Set(rule.requires)].filter((fact) => !available.has(fact));
105
+ issues.push({ code: "blocked-rule", severity, path: rule.path, contentId: rule.id,
106
+ message: `Content rule "${rule.id}" is blocked by ${missing.map((fact) => `"${fact}"`).join(", ")}.`,
107
+ repair: "Correct the prerequisites or add a reachable source. Declare legitimate external sources as initial facts; review intentional unavailable content." });
108
+ }
109
+ for (const requirement of input.required ?? []) {
110
+ if (available.has(requirement.id))
111
+ continue;
112
+ issues.push({ code: "unreachable-content", severity, path: requirement.path,
113
+ contentId: requirement.contentId, referenceId: requirement.id,
114
+ message: `Content "${requirement.contentId}" needs unavailable fact "${requirement.id}".`,
115
+ repair: "Correct this requirement or connect a source to the declared entry facts. If sources are incomplete, use open-world warnings until the game declares them." });
116
+ }
117
+ return { reachable: [...available], reachableRules, issues };
118
+ }
@@ -7,4 +7,12 @@ export declare function actionContextStack(ctx: GameContext): ActionContextStack
7
7
  /** Applies active contexts to a base action map for shell input tracking. */
8
8
  export declare function activeActionCodes(ctx: GameContext, base: ActionCodesMap): ActionCodesMap;
9
9
  export declare function setPlayControlsActive(ctx: GameContext, active: boolean): void;
10
+ /**
11
+ * Suspends this context's player controls until the returned release function is called.
12
+ * Independent owners compose; repeated release is safe and preserves the existing play gate.
13
+ * Dispose the lease when its menu closes or its owning context unmounts.
14
+ * Leases survive this context's binding restore and are excluded from saved binding state.
15
+ * @capability control-suspension Suspend player input across overlapping menus without stopping shared simulation.
16
+ */
17
+ export declare function suspendPlayControls(ctx: GameContext): () => void;
10
18
  export declare function playControlsActive(ctx: GameContext): boolean;
@@ -1,13 +1,25 @@
1
1
  import { createActionContextStack } from "../input/actionContexts.js";
2
2
  export const PLAY_CONTROLS_STORE_KEY = "jg.playControls";
3
+ const SUSPENSION_PREFIX = "__jg_control_suspension:";
3
4
  const ACTION_CONTEXTS = new WeakMap();
4
5
  function contextsFor(ctx) {
5
- let stack = ACTION_CONTEXTS.get(ctx);
6
- if (stack === undefined) {
7
- stack = createActionContextStack();
8
- ACTION_CONTEXTS.set(ctx, stack);
6
+ let state = ACTION_CONTEXTS.get(ctx);
7
+ if (state === undefined) {
8
+ const stack = createActionContextStack();
9
+ state = {
10
+ nextLease: 0,
11
+ stack: {
12
+ ...stack,
13
+ snapshot: () => ({ contexts: stack.snapshot().contexts.filter((context) => !context.id.startsWith(SUSPENSION_PREFIX)) }),
14
+ restore(snapshot) {
15
+ const leases = stack.snapshot().contexts.filter((context) => context.id.startsWith(SUSPENSION_PREFIX));
16
+ stack.restore({ contexts: [...snapshot.contexts.filter((context) => !context.id.startsWith(SUSPENSION_PREFIX)), ...leases] });
17
+ },
18
+ },
19
+ };
20
+ ACTION_CONTEXTS.set(ctx, state);
9
21
  }
10
- return stack;
22
+ return state.stack;
11
23
  }
12
24
  /** Returns the context stack associated with a game context. */
13
25
  export function actionContextStack(ctx) {
@@ -16,6 +28,8 @@ export function actionContextStack(ctx) {
16
28
  /** Applies active contexts to a base action map for shell input tracking. */
17
29
  export function activeActionCodes(ctx, base) {
18
30
  const stack = contextsFor(ctx);
31
+ if (stack.ids().some((id) => id.startsWith(SUSPENSION_PREFIX)))
32
+ return {};
19
33
  const layered = createActionContextStack();
20
34
  layered.push({ id: "__jg_base_actions", codes: base, passthrough: true });
21
35
  for (const context of stack.snapshot().contexts)
@@ -30,7 +44,30 @@ export function setPlayControlsActive(ctx, active) {
30
44
  stack.push({ id: "menu", codes: {}, passthrough: false });
31
45
  ctx.game.store.set(PLAY_CONTROLS_STORE_KEY, active);
32
46
  }
47
+ /**
48
+ * Suspends this context's player controls until the returned release function is called.
49
+ * Independent owners compose; repeated release is safe and preserves the existing play gate.
50
+ * Dispose the lease when its menu closes or its owning context unmounts.
51
+ * Leases survive this context's binding restore and are excluded from saved binding state.
52
+ * @capability control-suspension Suspend player input across overlapping menus without stopping shared simulation.
53
+ */
54
+ export function suspendPlayControls(ctx) {
55
+ const stack = contextsFor(ctx);
56
+ const state = ACTION_CONTEXTS.get(ctx);
57
+ let id;
58
+ do {
59
+ id = `${SUSPENSION_PREFIX}${state.nextLease++}`;
60
+ } while (stack.ids().includes(id));
61
+ stack.push({ id, codes: {}, passthrough: false });
62
+ let released = false;
63
+ return () => {
64
+ if (released)
65
+ return;
66
+ released = true;
67
+ stack.pop(id);
68
+ };
69
+ }
33
70
  export function playControlsActive(ctx) {
34
71
  return ctx.game.store.get(PLAY_CONTROLS_STORE_KEY) !== false &&
35
- !actionContextStack(ctx).snapshot().contexts.some((context) => context.id === "menu");
72
+ !actionContextStack(ctx).ids().some((id) => id === "menu" || id.startsWith(SUSPENSION_PREFIX));
36
73
  }
@@ -1,3 +1,5 @@
1
+ import type { EditorDocument } from "../editor/types.js";
2
+ import type { ProjectileTravelDeps } from "../combat/projectiles.js";
1
3
  import type { ActionCodesMap } from "../input/actionBindings.js";
2
4
  import type { GameFeedOptions } from "./feed.js";
3
5
  import type { GamePhase } from "./gamePhase.js";
@@ -6,7 +8,7 @@ import type { ItemTraits } from "../inventory/inventoryModel.js";
6
8
  import type { StorageTier } from "../inventory/storageTier.js";
7
9
  import type { GameContext } from "../runtime/gameContext.js";
8
10
  import type { ReplicationPolicy } from "../runtime/worldProjection.js";
9
- import type { RuntimeSaveMode } from "../runtime/runtimeSave.js";
11
+ import type { RuntimeSaveMode, RuntimeSaveOptions } from "../runtime/runtimeSave.js";
10
12
  import type { SaveConfig } from "../runtime/save.js";
11
13
  import { type AssetCatalog, type ModelAssetRef } from "../scene/assetCatalog.js";
12
14
  import { type EntityStore } from "../scene/entityStore.js";
@@ -22,8 +24,10 @@ export interface PersistConfig {
22
24
  mode?: RuntimeSaveMode;
23
25
  /** `"local"` (default) persists to `localStorage`; `"memory"` keeps saves in-session only (tests, "no persistence" mode). */
24
26
  storage?: "local" | "memory";
25
- /** Save-format version; bump when the world shape changes in a save-breaking way. */
27
+ /** Save-format version. Supply `migrate` for incompatible older shapes; without it, mismatched versions retain compatible load behavior. */
26
28
  version?: number;
29
+ /** Convert a mismatched version (or raw payload, version 0) before world restoration. Throw to reject without hydrating or changing the stored save. Current-version payloads bypass this hook. */
30
+ migrate?: RuntimeSaveOptions["migrate"];
27
31
  /** Autosave debounce in ms (autosave mode). Default 3000. */
28
32
  autosaveMs?: number;
29
33
  }
@@ -150,7 +154,7 @@ export interface LifecycleConfig<TState = unknown> {
150
154
  /**
151
155
  * How a game declares its run-phase story to {@link defineGameDefinition}. Every game states this
152
156
  * explicitly so the shell never has to guess — a silent game used to default to `"playing"`, which
153
- * painted the touch dock over title/menu/results screens (#1337, Vice Isle #1329).
157
+ * painted the touch dock over title/menu/results screens (#1337, Harbor Heat #1329).
154
158
  *
155
159
  * - A {@link LifecycleConfig} — the game moves through menu/playing/paused/ended run states; the
156
160
  * runtime owns the command glue and phase sync.
@@ -161,6 +165,17 @@ export interface LifecycleConfig<TState = unknown> {
161
165
  * this unset.
162
166
  */
163
167
  export type GameLifecycle<TState = unknown> = LifecycleConfig<TState> | "always-live";
168
+ /** Context-aware projectile policy; callbacks are not included in saved shot state. */
169
+ export interface GameProjectileTravelOptions extends Pick<ProjectileTravelDeps, "maxActive" | "maxRetained"> {
170
+ /** Collision-source limit, default 2048; an oversized source throws rather than dropping targets. */
171
+ maxTargets?: number;
172
+ /** Override the default centerline cover query with a radius-aware collision provider. */
173
+ sweep?: (ctx: GameContext, ...args: Parameters<NonNullable<ProjectileTravelDeps["sweep"]>>) => ReturnType<NonNullable<ProjectileTravelDeps["sweep"]>>;
174
+ /** Full acceleration override; call ctx.environment.accelerationAt explicitly to compose authored forces. */
175
+ acceleration?: (ctx: GameContext, ...args: Parameters<NonNullable<ProjectileTravelDeps["acceleration"]>>) => ReturnType<NonNullable<ProjectileTravelDeps["acceleration"]>>;
176
+ /** Bounded entity IDs for the default moving-target query; omission uses this world's entities. */
177
+ targets?(ctx: GameContext): readonly string[];
178
+ }
164
179
  /** Fully-resolved game description produced by {@link defineGameDefinition} — assets, scene, and opted-in subsystems. */
165
180
  export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> {
166
181
  name: string;
@@ -183,6 +198,20 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
183
198
  * pure UI/rules games that are not a spatial place.
184
199
  */
185
200
  world?: WorldFeature;
201
+ /** Validated authored scene inputs consumed by the environmental authority. */
202
+ authoredDocument?: EditorDocument;
203
+ /** Bounded projectile travel providers receive their live world; the authoritative clock stays engine-owned. */
204
+ projectileTravel?: GameProjectileTravelOptions;
205
+ /** Optional pursuit target policy; callbacks are world-local injections, never saved behavior data. */
206
+ pursuit?: {
207
+ /** Additional eligibility for nearest, held, explicit and forced targets at scheduled pursuit ticks.
208
+ * Receives the live world and entity IDs; cannot bypass self/existence/effect acceptance checks.
209
+ * Omission retains player-role acquisition; explicit targets may still name nonplayers.
210
+ * A supplied callback must return exactly true to accept; missing, null or asynchronous results deny.
211
+ * Keep this pair query bounded (for example, a caller-owned faction roster lookup).
212
+ */
213
+ eligible(ctx: GameContext, selfId: string, candidateId: string): boolean;
214
+ };
186
215
  /** Game-level default physics laws; a place world's own `physics` resolves over this. */
187
216
  physics?: PhysicsConfig;
188
217
  /** Simulation clock: real→game time scale, selectable speeds, calendar. Exposed as `ctx.time`; the shell feeds its scaled dt to `loop.onTick`. */
@@ -36,6 +36,26 @@ export interface DialogueGraph {
36
36
  /** Every node in the conversation. */
37
37
  nodes: readonly DialogueGraphNode[];
38
38
  }
39
+ /** A structural authoring issue, located in the original graph's node/choice arrays. */
40
+ export interface DialogueGraphIssue {
41
+ code: "duplicate-node" | "missing-start" | "missing-target" | "unreachable-node" | "no-exit";
42
+ severity: "error" | "warning";
43
+ message: string;
44
+ nodeId?: string;
45
+ nodeIndex?: number;
46
+ choiceIndex?: number;
47
+ targetId?: string;
48
+ }
49
+ /**
50
+ * Check authored dialogue references, reachability, and routes to an ending in O(nodes + choices).
51
+ * A node without choices, or a choice with no target, is an ending. Loops with an exit are valid.
52
+ * Duplicate ids and missing references are errors; unreachable nodes and closed loops are warnings
53
+ * because games may jump to nodes or intentionally keep conversations open. Topology uses the first
54
+ * node for each id, matching {@link selectDialogueView}. This does not evaluate story semantics.
55
+ *
56
+ * @capability dialogue-graph-validation diagnose duplicate ids, broken branches, unreachable dialogue, and nodes without a route to an ending before play
57
+ */
58
+ export declare function validateDialogueGraph(graph: DialogueGraph): DialogueGraphIssue[];
39
59
  /**
40
60
  * The render-ready snapshot of a conversation at one node — everything a view needs to
41
61
  * draw speaker, line, and choice buttons, with no traversal logic in the component.
@@ -1,3 +1,110 @@
1
+ /**
2
+ * Check authored dialogue references, reachability, and routes to an ending in O(nodes + choices).
3
+ * A node without choices, or a choice with no target, is an ending. Loops with an exit are valid.
4
+ * Duplicate ids and missing references are errors; unreachable nodes and closed loops are warnings
5
+ * because games may jump to nodes or intentionally keep conversations open. Topology uses the first
6
+ * node for each id, matching {@link selectDialogueView}. This does not evaluate story semantics.
7
+ *
8
+ * @capability dialogue-graph-validation diagnose duplicate ids, broken branches, unreachable dialogue, and nodes without a route to an ending before play
9
+ */
10
+ export function validateDialogueGraph(graph) {
11
+ const issues = [];
12
+ const nodes = new Map();
13
+ for (let index = 0; index < graph.nodes.length; index++) {
14
+ const node = graph.nodes[index];
15
+ if (nodes.has(node.id)) {
16
+ issues.push({
17
+ code: "duplicate-node", severity: "error", nodeId: node.id, nodeIndex: index,
18
+ message: `Duplicate dialogue node "${node.id}".`,
19
+ });
20
+ }
21
+ else {
22
+ nodes.set(node.id, { node, index });
23
+ }
24
+ }
25
+ if (!nodes.has(graph.start)) {
26
+ issues.push({
27
+ code: "missing-start", severity: "error", targetId: graph.start,
28
+ message: `Dialogue start "${graph.start}" does not exist.`,
29
+ });
30
+ }
31
+ for (let index = 0; index < graph.nodes.length; index++) {
32
+ const node = graph.nodes[index];
33
+ const choices = node.choices ?? [];
34
+ for (let choiceIndex = 0; choiceIndex < choices.length; choiceIndex++) {
35
+ const target = choices[choiceIndex].to;
36
+ if (target == null || nodes.has(target))
37
+ continue;
38
+ issues.push({
39
+ code: "missing-target", severity: "error", nodeId: node.id, nodeIndex: index,
40
+ choiceIndex, targetId: target,
41
+ message: `Dialogue choice targets unknown node "${target}".`,
42
+ });
43
+ }
44
+ }
45
+ const predecessors = new Map();
46
+ const canEnd = new Set();
47
+ const endings = [];
48
+ for (const { node } of nodes.values()) {
49
+ const choices = node.choices ?? [];
50
+ let ending = choices.length === 0;
51
+ for (const choice of choices) {
52
+ if (choice.to == null) {
53
+ ending = true;
54
+ }
55
+ else if (nodes.has(choice.to)) {
56
+ let incoming = predecessors.get(choice.to);
57
+ if (incoming === undefined) {
58
+ incoming = [];
59
+ predecessors.set(choice.to, incoming);
60
+ }
61
+ incoming.push(node.id);
62
+ }
63
+ }
64
+ if (ending) {
65
+ canEnd.add(node.id);
66
+ endings.push(node.id);
67
+ }
68
+ }
69
+ for (let head = 0; head < endings.length; head++) {
70
+ for (const id of predecessors.get(endings[head]) ?? []) {
71
+ if (canEnd.has(id))
72
+ continue;
73
+ canEnd.add(id);
74
+ endings.push(id);
75
+ }
76
+ }
77
+ const reachable = new Set();
78
+ const pending = [];
79
+ if (nodes.has(graph.start)) {
80
+ reachable.add(graph.start);
81
+ pending.push(graph.start);
82
+ }
83
+ for (let head = 0; head < pending.length; head++) {
84
+ for (const choice of nodes.get(pending[head]).node.choices ?? []) {
85
+ const target = choice.to;
86
+ if (target == null || !nodes.has(target) || reachable.has(target))
87
+ continue;
88
+ reachable.add(target);
89
+ pending.push(target);
90
+ }
91
+ }
92
+ for (const { node, index } of nodes.values()) {
93
+ if (nodes.has(graph.start) && !reachable.has(node.id)) {
94
+ issues.push({
95
+ code: "unreachable-node", severity: "warning", nodeId: node.id, nodeIndex: index,
96
+ message: `Dialogue node "${node.id}" is unreachable from the start.`,
97
+ });
98
+ }
99
+ if (!canEnd.has(node.id)) {
100
+ issues.push({
101
+ code: "no-exit", severity: "warning", nodeId: node.id, nodeIndex: index,
102
+ message: `Dialogue node "${node.id}" has no route to an ending.`,
103
+ });
104
+ }
105
+ }
106
+ return issues;
107
+ }
1
108
  /**
2
109
  * Project a {@link DialogueGraph} node id to its render-ready {@link DialogueGraphView} —
3
110
  * the pure "current view" selector a stateless renderer reads (speaker, line, choices,
@@ -1,3 +1,4 @@
1
+ import type { EffectResult } from "../combat/effects.js";
1
2
  import type { DeathReason } from "../combat/deathReason.js";
2
3
  import type { TelegraphShape } from "../combat/telegraph.js";
3
4
  import type { CameraShake } from "../combat/hitReaction.js";
@@ -159,7 +160,11 @@ export interface CombatHitReactionEvent {
159
160
  /** Trauma (0..1) for a trauma² shake channel — set when the reaction resolved from `trauma` instead of `shake`. */
160
161
  trauma?: number;
161
162
  }
163
+ /** Exactly-once projectile result for shot-specific game policy and presentation. */
162
164
  export interface ProjectileSettledEvent {
165
+ shotId: string;
166
+ /** Actual applied effects; empty for misses and cover impacts. */
167
+ hits: readonly EffectResult[];
163
168
  from: string;
164
169
  origin: [number, number, number];
165
170
  at: [number, number, number];
@@ -4,8 +4,21 @@ export interface KeyValueStorage {
4
4
  setItem(key: string, value: string): void;
5
5
  removeItem(key: string): void;
6
6
  }
7
- /** The ambient `localStorage` as a {@link KeyValueStorage} when one exists (browser), otherwise `null` (server/tests) — never references the DOM `Storage` type, so it is safe in core. */
8
- export declare function defaultKeyValueStorage(): KeyValueStorage | null;
7
+ /** The ambient `localStorage` when available, otherwise `null`. An optional observer receives denied getter errors; it may rethrow to reject fallback. Never references the DOM `Storage` type, so it is safe in core. */
8
+ export declare function defaultKeyValueStorage(onError?: (error: unknown) => void): KeyValueStorage | null;
9
+ /** A failed storage operation, retaining the backend's original error and affected key. */
10
+ export interface KeyValueStorageFailure {
11
+ operation: "read" | "write" | "remove";
12
+ key: string;
13
+ error: unknown;
14
+ }
15
+ /** Fallback keeps session state after a failed operation; throw exposes failure and preserves the previous cell value. Observers must not throw. */
16
+ export interface KeyValueStorageErrorPolicy {
17
+ errorMode?: "fallback" | "throw";
18
+ onError?: (failure: KeyValueStorageFailure) => void;
19
+ }
20
+ /** @internal */
21
+ export declare function reportStorageFailure(policy: KeyValueStorageErrorPolicy, failure: KeyValueStorageFailure): void;
9
22
  /** A single persisted, mutable cell: read the current value, overwrite it, or read-modify-write with {@link KeyValueStore.update}. Unlike a record book it has no monotonic guard — the value goes wherever you set it. */
10
23
  export interface KeyValueStore<T> {
11
24
  get(): T;
@@ -14,12 +27,12 @@ export interface KeyValueStore<T> {
14
27
  clear(): void;
15
28
  }
16
29
  /** Config for {@link createKeyValueStore}: the storage `key`, the `initial` value used before anything is saved, an optional `storage` backend (defaults to `localStorage`, pass `null` for memory-only), and optional custom `serialize`/`deserialize` (default JSON). */
17
- export interface KeyValueStoreConfig<T> {
30
+ export interface KeyValueStoreConfig<T> extends KeyValueStorageErrorPolicy {
18
31
  readonly key: string;
19
32
  readonly initial: T;
20
33
  readonly storage?: KeyValueStorage | null;
21
34
  readonly serialize?: (value: T) => string;
22
35
  readonly deserialize?: (raw: string) => T;
23
36
  }
24
- /** A lightweight mutable local save cell for single-player state (a credit bank, a settings blob, level progress) — the read-modify-write counterpart to the monotonic `recordBook`. Persists through a {@link KeyValueStorage} (browser `localStorage` by default); corrupt or unavailable storage degrades to in-memory and never throws into a game tick. */
37
+ /** A mutable local save cell through {@link KeyValueStorage}. Default fallback keeps session state on failure; `errorMode: "throw"` exposes failed reads/writes/removals through the original error. `storage: null` deliberately uses memory only. */
25
38
  export declare function createKeyValueStore<T>(config: KeyValueStoreConfig<T>): KeyValueStore<T>;
@@ -1,42 +1,54 @@
1
- /** The ambient `localStorage` as a {@link KeyValueStorage} when one exists (browser), otherwise `null` (server/tests) — never references the DOM `Storage` type, so it is safe in core. */
2
- export function defaultKeyValueStorage() {
1
+ /** The ambient `localStorage` when available, otherwise `null`. An optional observer receives denied getter errors; it may rethrow to reject fallback. Never references the DOM `Storage` type, so it is safe in core. */
2
+ export function defaultKeyValueStorage(onError) {
3
3
  try {
4
4
  if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
5
5
  return globalThis.localStorage ?? null;
6
6
  }
7
7
  }
8
- catch {
8
+ catch (error) {
9
+ onError?.(error);
9
10
  return null;
10
11
  }
11
12
  return null;
12
13
  }
13
- /** A lightweight mutable local save cell for single-player state (a credit bank, a settings blob, level progress) — the read-modify-write counterpart to the monotonic `recordBook`. Persists through a {@link KeyValueStorage} (browser `localStorage` by default); corrupt or unavailable storage degrades to in-memory and never throws into a game tick. */
14
+ /** @internal */
15
+ export function reportStorageFailure(policy, failure) {
16
+ policy.onError?.(failure);
17
+ if (policy.errorMode === "throw")
18
+ throw failure.error;
19
+ }
20
+ /** A mutable local save cell through {@link KeyValueStorage}. Default fallback keeps session state on failure; `errorMode: "throw"` exposes failed reads/writes/removals through the original error. `storage: null` deliberately uses memory only. */
14
21
  export function createKeyValueStore(config) {
15
- const storage = config.storage === undefined ? defaultKeyValueStorage() : config.storage;
22
+ const fail = (operation, error) => reportStorageFailure(config, { operation, key: config.key, error });
23
+ const storage = config.storage === undefined ? defaultKeyValueStorage((error) => fail("read", error)) : config.storage;
16
24
  const serialize = config.serialize ?? ((value) => JSON.stringify(value));
17
25
  const deserialize = config.deserialize ?? ((raw) => JSON.parse(raw));
18
26
  const read = () => {
19
- if (storage === null)
27
+ if (storage === null) {
28
+ if (config.storage !== null && config.errorMode === "throw")
29
+ fail("read", new Error("Local storage is unavailable"));
20
30
  return config.initial;
31
+ }
21
32
  try {
22
33
  const raw = storage.getItem(config.key);
23
34
  return raw === null ? config.initial : deserialize(raw);
24
35
  }
25
- catch {
36
+ catch (error) {
37
+ fail("read", error);
26
38
  return config.initial;
27
39
  }
28
40
  };
29
41
  let current = read();
30
42
  const write = (value) => {
31
- current = value;
32
- if (storage === null)
33
- return;
34
- try {
35
- storage.setItem(config.key, serialize(value));
36
- }
37
- catch {
38
- // storage rejected the write; keep the value in memory for this session
43
+ if (storage !== null) {
44
+ try {
45
+ storage.setItem(config.key, serialize(value));
46
+ }
47
+ catch (error) {
48
+ fail("write", error);
49
+ }
39
50
  }
51
+ current = value;
40
52
  };
41
53
  return {
42
54
  get: () => current,
@@ -47,15 +59,15 @@ export function createKeyValueStore(config) {
47
59
  return next;
48
60
  },
49
61
  clear: () => {
50
- current = config.initial;
51
- if (storage === null)
52
- return;
53
- try {
54
- storage.removeItem(config.key);
55
- }
56
- catch {
57
- // ignore removal failure; in-memory value already reset
62
+ if (storage !== null) {
63
+ try {
64
+ storage.removeItem(config.key);
65
+ }
66
+ catch (error) {
67
+ fail("remove", error);
68
+ }
58
69
  }
70
+ current = config.initial;
59
71
  },
60
72
  };
61
73
  }
@@ -0,0 +1,17 @@
1
+ import type { LootEntry } from "./lootTable.js";
2
+ import type { LootModifier } from "./lootPipeline.js";
3
+ /** Caller-authored weighting curve; item ids, quality tiers and balance never live in the modifier. */
4
+ export interface TimeScaledRarityOptions {
5
+ id?: string;
6
+ ramp: (entry: LootEntry, hours: number) => number;
7
+ }
8
+ /**
9
+ * Multiply weighted odds (or independent chances, clamped to one) by a caller-authored elapsed-time
10
+ * curve. Keeps source tables unchanged and uses the existing pipeline's modifier provenance.
11
+ * Elapsed time and multipliers must be finite and nonnegative; invalid curves reject the roll.
12
+ *
13
+ * @capability time-scaled-loot shape loot odds by elapsed hours with caller-authored curves and pipeline provenance
14
+ */
15
+ export declare function timeScaledRarity<TCtx extends {
16
+ elapsedMs: number;
17
+ }>(options: TimeScaledRarityOptions): LootModifier<TCtx>;