@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
@@ -65,6 +65,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
65
65
 
66
66
  - `createItemUse` (function) · `import { createItemUse } from "@jgengine/core/gameplay"`
67
67
 
68
+ ## content-progression-validation — diagnose unavailable sources and blocked cross-content progression under explicit game-owned rules without rejecting intentional seeded cycles
69
+
70
+ - `validateContentProgression` (function) · `import { validateContentProgression } from "@jgengine/core/game/contentValidation"`
71
+
72
+ ## content-reference-validation — locate duplicate content ids and broken references across game-owned catalog namespaces with repair guidance
73
+
74
+ - `validateContentReferences` (function) · `import { validateContentReferences } from "@jgengine/core/game/contentValidation"`
75
+
76
+ ## control-suspension — Suspend player input across overlapping menus without stopping shared simulation.
77
+
78
+ - `suspendPlayControls` (function) · `import { suspendPlayControls } from "@jgengine/core/game/controlGate"`
79
+
68
80
  ## cosmetics — equip cosmetic skins and customizations by slot
69
81
 
70
82
  - `createCosmetics` (function) · `import { createCosmetics } from "@jgengine/core/gameplay"`
@@ -112,6 +124,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
112
124
 
113
125
  - `createGameDialogue` (function) · `import { createGameDialogue } from "@jgengine/core/gameplay"`
114
126
 
127
+ ## dialogue-graph-validation — diagnose duplicate ids, broken branches, unreachable dialogue, and nodes without a route to an ending before play
128
+
129
+ - `validateDialogueGraph` (function) · `import { validateDialogueGraph } from "@jgengine/core/game/dialogueGraph"`
130
+
115
131
  ## dialogue-graph-view — select a conversation node's render-ready view (speaker, line, branching choices) from a serializable dialogue graph
116
132
 
117
133
  - `selectDialogueView` (function) · `import { selectDialogueView } from "@jgengine/core/gameplay"`
@@ -144,6 +160,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
144
160
 
145
161
  - `createEventTicker` (function) · `import { createEventTicker } from "@jgengine/core/gameplay"`
146
162
 
163
+ ## expedition-dispatch — dispatch a saveable timed away mission with finite supplies and a seeded RNG cursor
164
+
165
+ - `dispatchExpedition` (function) · `import { dispatchExpedition } from "@jgengine/core/gameplay"`
166
+
167
+ ## expedition-recall — begin a safe fractional-time return after settling the pending expedition frontier
168
+
169
+ - `recallExpedition` (function) · `import { recallExpedition } from "@jgengine/core/gameplay"`
170
+
171
+ ## expedition-return — complete an authorized instant return without skipping unsettled ticks or reviving death
172
+
173
+ - `returnExpeditionNow` (function) · `import { returnExpeditionNow } from "@jgengine/core/gameplay"`
174
+
175
+ ## expedition-settle — settle bounded offline expedition ticks without losing supplies rewards or RNG continuity
176
+
177
+ - `settleExpedition` (function) · `import { settleExpedition } from "@jgengine/core/gameplay"`
178
+
147
179
  ## fire-input — bind a key or mouse button to a named action that runs the same-named command with the aim; repeatMs auto-fires while held
148
180
 
149
181
  - `ActionCodesMap` (type) · `import { ActionCodesMap } from "@jgengine/core/gameplay"`
@@ -289,6 +321,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
289
321
 
290
322
  - `createNameGenerator` (function) · `import { createNameGenerator } from "@jgengine/core/gameplay"`
291
323
 
324
+ ## narrative-batch-validation — locate stale canon, oversized batches, explicit character/fact/chronology contradictions and dialogue graph issues for bounded repair
325
+
326
+ - `validateNarrativeBatch` (function) · `import { validateNarrativeBatch } from "@jgengine/core/game/narrativeAuthoring"`
327
+
328
+ ## narrative-canon — freeze caller-reviewed character ids, facts and chronology for a narrative authoring revision
329
+
330
+ - `freezeNarrativeCanon` (function) · `import { freezeNarrativeCanon } from "@jgengine/core/game/narrativeAuthoring"`
331
+
292
332
  ## notice-feed — serializable tone-tagged notice/event feed bounded by count and age (toasts, life events, killfeed)
293
333
 
294
334
  - `appendFeed` (function) · `import { appendFeed } from "@jgengine/core/gameplay"`
@@ -372,6 +412,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
372
412
 
373
413
  - `evalCurve` (function) · `import { evalCurve } from "@jgengine/core/gameplay"`
374
414
 
415
+ ## quest-catalog-validation — diagnose authored quest ids, prerequisite and reward references, quantities, and blocked dependencies with caller-declared external unlocks
416
+
417
+ - `validateQuestCatalog` (function) · `import { validateQuestCatalog } from "@jgengine/core/game/questCatalog"`
418
+
375
419
  ## quest-log — track accepted quests and their per-objective progress
376
420
 
377
421
  - `createQuestJournal` (function) · `import { createQuestJournal } from "@jgengine/core/gameplay"`
@@ -393,6 +437,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
393
437
 
394
438
  - `createRaceState` (function) · `import { createRaceState } from "@jgengine/core/gameplay"`
395
439
 
440
+ ## recipe-catalog-validation — locate broken crafting item/station/unlock references, duplicate inputs and invalid authored quantities with repair guidance
441
+
442
+ - `validateRecipeCatalog` (function) · `import { validateRecipeCatalog } from "@jgengine/core/crafting/recipeCatalog"`
443
+
396
444
  ## resolve — the current tier/band label for a keyed or scalar value
397
445
 
398
446
  - `tierAt` (function) · `import { tierAt } from "@jgengine/core/gameplay"`
@@ -443,6 +491,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
443
491
 
444
492
  - `setValue` (function) · `import { setValue } from "@jgengine/core/gameplay"`
445
493
 
494
+ ## shaped-grid — a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
495
+
496
+ - `canPlace` (function) · `import { canPlace } from "@jgengine/core/inventory/shapedGrid"`
497
+ - `createShapedGrid` (function) · `import { createShapedGrid } from "@jgengine/core/inventory/shapedGrid"`
498
+ - `moveShaped` (function) · `import { moveShaped } from "@jgengine/core/inventory/shapedGrid"`
499
+ - `placeShaped` (function) · `import { placeShaped } from "@jgengine/core/inventory/shapedGrid"`
500
+ - `removeShaped` (function) · `import { removeShaped } from "@jgengine/core/inventory/shapedGrid"`
501
+
502
+ ## shaped-placement — find a shaped inventory item's first fit with ordered rotations, row-major placement, and an explicit search budget
503
+
504
+ - `findShapedPlacement` (function) · `import { findShapedPlacement } from "@jgengine/core/inventory/shapedGrid"`
505
+
446
506
  ## shop-grid — serializable vendor/shop stock with buy/sell over a caller-owned wallet — finite/unlimited qty, sell-back, restock/setPrice, snapshot/restore
447
507
 
448
508
  - `createShopStock` (function) · `import { createShopStock } from "@jgengine/core/gameplay"`
@@ -459,6 +519,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
459
519
 
460
520
  - `splitViewports` (function) · `import { splitViewports } from "@jgengine/core/game/viewports"`
461
521
 
522
+ ## staffed-station-rate — turn bounded caller-owned worker stats and efficiency into a production rate
523
+
524
+ - `stationOutputRate` (function) · `import { stationOutputRate } from "@jgengine/core/gameplay"`
525
+
462
526
  ## stat-graph — derive game-defined stats from named inputs with provenance and preview
463
527
 
464
528
  - `createStatGraph` (function) · `import { createStatGraph } from "@jgengine/core/gameplay"`
@@ -483,6 +547,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
483
547
 
484
548
  - `tick` (function) · `import { tick } from "@jgengine/core/gameplay"`
485
549
 
550
+ ## time-scaled-loot — shape loot odds by elapsed hours with caller-authored curves and pipeline provenance
551
+
552
+ - `timeScaledRarity` (function) · `import { timeScaledRarity } from "@jgengine/core/gameplay"`
553
+
486
554
  ## toast-feed — queue of transient self-expiring on-screen messages (toasts, announcer, kill-feed)
487
555
 
488
556
  - `appendToast` (function) · `import { appendToast } from "@jgengine/core/gameplay"`
@@ -0,0 +1,46 @@
1
+ # Truthful device saves
2
+
3
+ Use `ctx.game.save` for the authoritative world or `createSaveStore` for a caller-owned,
4
+ serializable checkpoint. Default offline context saves use strict local storage:
5
+ denied reads, quota errors, unavailable storage, and denied removals report
6
+ `status() === "error"`. Explicitly injected backends retain their own policy.
7
+
8
+ For an existing Wayfarer/OddOrbit-style run checkpoint, keep the game's checkpoint
9
+ schema and validation, and replace only its persistence adapter:
10
+
11
+ ```ts
12
+ import { createSaveStore, localSaveBackend } from "@jgengine/core/game/saveStore";
13
+
14
+ const checkpoint = createSaveStore({
15
+ key: "my-game:run",
16
+ initial: { roomId: "start", cleared: [] as string[] },
17
+ backend: localSaveBackend(undefined, { errorMode: "throw" }),
18
+ onError: (error) => console.error("Checkpoint storage failed", error),
19
+ });
20
+
21
+ checkpoint.set({ roomId: "harbor", cleared: ["intro"] });
22
+ await checkpoint.save();
23
+ const message = checkpoint.status() === "saved"
24
+ ? "Checkpoint saved"
25
+ : "Checkpoint could not be saved. Keep playing and retry.";
26
+ ```
27
+
28
+ `save()` handles backend rejection; awaiting it alone does not prove success.
29
+ Subscribe to status changes for autosave UI. A rejected payload write preserves
30
+ the previous stored checkpoint while live edits remain available for retry.
31
+ Denied load/remove leaves the current value intact. Whole-world load/hasSave/slot
32
+ switches return false after a denied payload read and do not restore cached state. Slot metadata is a separate
33
+ storage operation: a payload can be written before its slot index fails. That
34
+ reports `error` too; there is no cross-key rollback or transaction guarantee.
35
+ Async index updates after load also report through status and `onError`.
36
+
37
+ `createKeyValueStore` and `localSaveBackend` preserve their legacy silent fallback
38
+ by default. Set `errorMode: "throw"` when persistence must succeed; optionally
39
+ provide `onError({ operation, key, error })` to observe reads, writes and removals.
40
+ Strict key-value writes/removals preserve the prior cell on rejection. Observers
41
+ must not throw. Shape validation and corruption recovery still belong to the
42
+ game's checkpoint schema or a supplied deserializer.
43
+
44
+ For intentional session-only/headless success, use `memorySaveBackend()`,
45
+ `persist: { storage: "memory" }`, or a key-value cell with `storage: null`.
46
+ Memory success does not promise persistence across reloads.
@@ -0,0 +1,108 @@
1
+ # Timed away missions
2
+
3
+ Use the pure transitions in `@jgengine/core/work/expedition` to orchestrate a timed
4
+ away mission. The caller owns its clock, store, identities, authored content and
5
+ policy. Core owns event ordering and the exploring → returning → home / dead
6
+ lifecycle, not a scheduler, genre preset or persistence backend.
7
+
8
+ All `*Ms` fields and log `at` timestamps are epoch milliseconds. Persist the entire
9
+ `ExpeditionState`, including `cursor`, supplies, carried rewards and the completed
10
+ tick frontier. `settleExpedition(config, state, nowMs)` uses `accrueSince` and
11
+ `steppedCatchUp`; a multi-day gap follows the same loot → hazard → ordered supplies
12
+ → death → capacity recall sequence as incremental calls. Each callback is pure
13
+ and receives the state at that event's time, with the persisted RNG stream.
14
+
15
+ ```ts
16
+ import { createLootPipeline } from "@jgengine/core/game/lootPipeline";
17
+ import { timeScaledRarity } from "@jgengine/core/game/lootModifiers";
18
+ import {
19
+ dispatchExpedition, settleExpedition, recallExpedition, returnExpeditionNow,
20
+ type ExpeditionConfig, type ExpeditionState,
21
+ } from "@jgengine/core/work/expedition";
22
+ import { defineStore } from "@jgengine/core/store/defineStore";
23
+ import { defineSystem } from "@jgengine/core/game/defineSystem";
24
+
25
+ const rewards = createLootPipeline<{ elapsedMs: number }>({
26
+ id: "survey-rewards",
27
+ stages: [{ id: "survey", table: {
28
+ id: "survey-pool", entries: [
29
+ { item: "sample", count: 1, weight: 9 },
30
+ { item: "artifact", count: 1, weight: 1 },
31
+ ],
32
+ }, modifiers: [timeScaledRarity({
33
+ id: "survey-depth",
34
+ ramp: (entry, hours) => entry.item === "artifact" ? 1 + hours : 1,
35
+ })] }],
36
+ });
37
+ const survey: ExpeditionConfig<{ elapsedMs: number }> = {
38
+ tickMs: 20 * 60_000,
39
+ loot: rewards,
40
+ lootCtx: state => ({ elapsedMs: state.elapsedMs }),
41
+ maxHp: state => Math.max(1, 100 - state.exposure),
42
+ hazard: (_state, rng) => ({ damage: rng() < 0.25 ? 4 : 1, exposure: 1 }),
43
+ supplies: [
44
+ { key: "filter", when: state => state.exposure >= 10, clearExposure: 10 },
45
+ { key: "medicine", when: state => state.hp < 30, restoreHp: 25 },
46
+ ],
47
+ returnFraction: 0.5,
48
+ carryCap: 100,
49
+ maxTicksPerSettle: 1000,
50
+ logLimit: 200,
51
+ describe: event => `Survey: ${event.kind}`, // Replace with authored/localized prose.
52
+ };
53
+
54
+ export function awayMission(nowMs: () => number) {
55
+ const mission = defineStore<ExpeditionState | null>("away-mission", null);
56
+ const dispatch = () => dispatchExpedition({
57
+ id: "surveyor", startAtMs: nowMs(), hp: 100,
58
+ supplies: { medicine: 5, filter: 2 }, seed: "caller-campaign:surveyor",
59
+ });
60
+ const system = defineSystem({
61
+ id: "away-mission", tick: { type: "frame" },
62
+ create(ctx) { mission.write(ctx, dispatch()); },
63
+ update(ctx) { mission.update(ctx, state => state === null ? null : settleExpedition(survey, state, nowMs())); },
64
+ reset(ctx) { mission.write(ctx, dispatch()); },
65
+ });
66
+ return { mission, system, recallExpedition, returnExpeditionNow };
67
+ }
68
+ ```
69
+
70
+ Install the returned system with `defineGame({ systems: [system] })`. Store writes
71
+ and `ctx.state()` / `ctx.restore()` keep the ordinary native save boundary; no
72
+ expedition-specific backend is required. A caller-owned store may instead call
73
+ the same pure transitions and atomically persist each returned value. Decode and
74
+ validate your saved schema at that boundary. Keep policy callbacks free of side
75
+ effects; a rejected policy transition does not write through the pure API.
76
+
77
+ A courier composition can use a parcel-only loot pipeline, no exposure,
78
+ `maxHp: () => 20`, rations restoring health, and `returnFraction: 0.25`. Both
79
+ compositions use the same orchestration. Item names, encounter notices, quality
80
+ curves, hazard magnitudes, immunity and supply thresholds are authored data and
81
+ callbacks; they are not SDK content presets. `timeScaledRarity` multiplies weighted
82
+ odds or independent chances (clamped to one) and preserves the loot pipeline's
83
+ original/effective odds and modifier ids. There is no built-in rarity taxonomy.
84
+
85
+ `carryCap` counts **item units**, not drop records. Rolled item quantities are
86
+ admitted in pipeline order up to the remaining cap; currency does not consume
87
+ item capacity and is carried in full. Carried counts merge by identity. Each loot
88
+ event's `drops` contains accepted quantities, while `loot` contains the full rolled
89
+ resolution and provenance, including quantities excluded by capacity. Core starts
90
+ auto-return at the event time that filled capacity. Returning never rolls rewards,
91
+ consumes supplies or takes hazard damage. Arrival is recorded at its exact deadline
92
+ even when one offline call observes it much later. Dead missions never revive.
93
+
94
+ `maxTicksPerSettle` bounds work without dropping elapsed ticks. If the budget runs
95
+ out, the completed frontier remains behind `nowMs`. Recall and instant return use
96
+ that same settlement pipeline and postpone the transition while due ticks remain;
97
+ repeat at the same target until caught up. The caller owns instant-return payment
98
+ and authorization. Count-bound `appendFeed` keeps the newest log events without
99
+ discarding carried rewards or RNG progress. Supply rules run in array order and
100
+ spend one unit only when restoration/cleansing changes a vital; health can recover
101
+ from zero during that tick before the death decision.
102
+
103
+ Configuration is runtime policy, never saved into state. Retuning applies the
104
+ current policy to unsettled ticks; a changed cadence anchors at the last completed
105
+ tick. Persist a policy/version id alongside your state if historical policy must
106
+ be replayed exactly. A lower carry cap preserves already-owned rewards and recalls
107
+ after the next due event (or immediately when no tick is due). Monotonic settlement
108
+ does not replay time when the caller's clock moves backward.
@@ -0,0 +1,76 @@
1
+ # Repair dialogue batches against reviewed canon
2
+
3
+ `npx jgengine recipe narrative-authoring` prints an SDK-typechecked function with
4
+ injected `generate` and `review` callbacks. Use it for a bounded chapter's
5
+ conversations, then stage the accepted batch for playtesting. It selects no
6
+ model, network service, assets or genre.
7
+
8
+ The author supplies a chapter brief: intended experience, voices, prerequisites,
9
+ player choices and their consequences. Keep reviewed canon separate from draft
10
+ prose. `freezeNarrativeCanon` detaches and freezes a revision, character ids,
11
+ exact string facts and a strict event order. Include only events whose order is
12
+ settled; branching or uncertain chronology belongs in semantic review. A new
13
+ fact or changed fact requires a separately reviewed canon revision.
14
+
15
+ ```ts
16
+ import {
17
+ freezeNarrativeCanon,
18
+ validateNarrativeBatch,
19
+ type NarrativeBatch,
20
+ } from "@jgengine/core/game/narrativeAuthoring";
21
+
22
+ const canon = freezeNarrativeCanon({
23
+ revision: "inquiry-r7",
24
+ characterIds: ["diver"],
25
+ facts: { hatch: "sealed" },
26
+ eventIds: ["descent", "alarm"],
27
+ });
28
+ const candidate: NarrativeBatch = {
29
+ canonRevision: canon.revision,
30
+ dialogues: [{
31
+ id: "deposition",
32
+ graph: {
33
+ start: "witness",
34
+ nodes: [{ id: "witness", speaker: "Witness 4", text: "The hatch stayed sealed." }],
35
+ },
36
+ claims: [{ nodeId: "witness", kind: "fact", factId: "hatch", value: "sealed" }],
37
+ }],
38
+ };
39
+ const issues = validateNarrativeBatch(canon, candidate, {
40
+ maxDialogues: 1, maxNodes: 20, maxChoices: 40, maxClaims: 30, maxTextCharacters: 6000,
41
+ });
42
+ ```
43
+
44
+ These numbers and content demonstrate caller choices. A procedural undersea
45
+ inquiry can hinge on preserving a witness's anonymity versus publishing evidence.
46
+ An absurd rooftop tea negotiation can hinge on saluting a pigeon's ceremonial
47
+ hat versus offering biscuits. Each game writes its own canon, voices, style and
48
+ consequences; neither slice supplies a reusable story preset.
49
+
50
+ The typed batch contains graphs plus explicit node-attached `character`, `fact`
51
+ and `before` claims. `validateNarrativeBatch` checks those claims and reuses
52
+ `validateDialogueGraph` for topology. Diagnostics include a JSON-pointer path,
53
+ for example `/dialogues/0/claims/1/value` or
54
+ `/dialogues/0/graph/nodes/2/choices/0/to`. Speaker labels and prose remain display
55
+ data. Omitted or misleading claims cannot prove prose is coherent.
56
+
57
+ The CLI function snapshots canon, limits, brief and callback selection before
58
+ awaiting generation. It checks revision and budgets before copying a returned
59
+ candidate; oversized and stale candidates are discarded with diagnostics.
60
+ Within-budget drafts and repair diagnostics are detached and frozen across
61
+ callbacks. Fix the located candidate, preserving the reviewed canon. Attempts
62
+ stop at the caller's positive `maxAttempts`; exhausting it returns `needs-review`.
63
+
64
+ Generators return typed `NarrativeBatch` data. Decode external JSON before this
65
+ boundary and enforce response byte/token budgets in the generator adapter; the
66
+ SDK's node/choice/claim/text limits are not a transport-size limiter. Callback
67
+ failures propagate. A pending reviewer stays pending, and rejection never implies
68
+ approval. A reviewer must return `approved: true` and no error diagnostics for
69
+ acceptance. Review topology warnings explicitly against intended play.
70
+
71
+ Semantic review compares the actual prose with canon and the chapter brief,
72
+ including unannotated statements, character motivations, causality, chronology,
73
+ lasting consequences and meaningful playable choices. Return located issues to
74
+ the next repair attempt. Play representative branches before promoting accepted
75
+ drafts to the game's authored catalog. Acceptance stages data; this function
76
+ never publishes content or replaces canon.
@@ -0,0 +1,101 @@
1
+ # Save generated items in bounded pages
2
+
3
+ Use this recipe when an existing project owns generated item storage and wants
4
+ runtime ids and incremental save export without mirroring its items into another
5
+ registry. The interface is synchronous; it does not provide a database adapter.
6
+
7
+ ```ts
8
+ import {
9
+ createItemInstanceRegistry,
10
+ type ItemInstanceStorage,
11
+ type ItemInstanceRegistryState,
12
+ } from "@jgengine/core/item/itemInstanceRegistry";
13
+
14
+ type RolledItem = { damage: number; affixes: string[] };
15
+
16
+ let existingItems = new Map<string, RolledItem>();
17
+ const storage: ItemInstanceStorage<RolledItem> = {
18
+ get: (id) => existingItems.get(id),
19
+ has: (id) => existingItems.has(id),
20
+ set: (id, definition) => void existingItems.set(id, definition),
21
+ delete: (id) => void existingItems.delete(id),
22
+ count: () => existingItems.size,
23
+ entries: () => existingItems.entries(),
24
+ replace(entries) {
25
+ const staged = new Map(entries);
26
+ existingItems = staged;
27
+ },
28
+ };
29
+
30
+ const registry = createItemInstanceRegistry("rolled", { storage });
31
+ const runtimeId = registry.register("blade", { damage: 17, affixes: ["keen"] });
32
+ const item = registry.get(runtimeId);
33
+ ```
34
+
35
+ The example adapter stages replacement in another Map. A larger caller-owned
36
+ store can stage in its own transaction or generation and switch on success.
37
+ `replace` must consume the entire iterable synchronously and preserve old data
38
+ when iteration, cloning, validation, or commit fails. Do not clear live data and
39
+ then populate it. Stream `entries()` in stable order; materializing all entries
40
+ inside the adapter defeats bounded export.
41
+
42
+ ## Export and restore
43
+
44
+ ```ts
45
+ function saveItems(writePage: (json: string) => void): void {
46
+ for (const page of registry.statePages(256)) {
47
+ writePage(JSON.stringify(page));
48
+ }
49
+ }
50
+
51
+ function restoreItems(readPages: Iterable<string>): boolean {
52
+ function* decodedPages(): IterableIterator<ItemInstanceRegistryState<RolledItem>> {
53
+ for (const json of readPages) {
54
+ yield JSON.parse(json) as ItemInstanceRegistryState<RolledItem>;
55
+ }
56
+ }
57
+ return registry.restorePages(decodedPages());
58
+ }
59
+ ```
60
+
61
+ Each page retains the existing `{ prefix, sequence, entries }` save shape.
62
+ An empty registry exports one empty page so released ids still retain their
63
+ allocation sequence. Every restored page must have the same prefix and sequence.
64
+ Each page's definitions are detached independently; cross-page object identity
65
+ is not preserved. Keep definitions JSON-compatible for JSON saves.
66
+
67
+ `statePages` captures the allocator when called, reads at most `pageSize` entries
68
+ per step, and clones only that batch. Registry mutations invalidate the export
69
+ and the next step throws. Abandoning an iterator never blocks writes; call its
70
+ `return()` when stopping early to release adapter resources. Keep direct writes
71
+ to injected storage and definition objects quiescent during export: the registry
72
+ cannot detect them. The caller owns immutable capture or staging if saves must
73
+ overlap live changes. Publish a set of pages only after the complete export
74
+ succeeds; discard partial saves on failure.
75
+
76
+ `restorePages` returns false for an empty iterable or an invalid first header.
77
+ Later inconsistent headers, clone failures, and storage failures throw. A
78
+ conforming adapter keeps both the old collection and allocator intact on failure.
79
+ Restore consumes and clones one input page at a time; storage staging cost belongs
80
+ to the adapter. Existing `state()` and `restore(state)` remain whole-collection
81
+ operations and retain their original save shape.
82
+
83
+ ## Reattach existing storage
84
+
85
+ If the caller restores storage separately, persist the page's allocator header
86
+ alongside it and attach with its original prefix and sequence:
87
+
88
+ ```ts
89
+ function reattach(savedHeader: { prefix: string; sequence: number }) {
90
+ return createItemInstanceRegistry(savedHeader.prefix, {
91
+ storage,
92
+ sequence: savedHeader.sequence,
93
+ });
94
+ }
95
+ ```
96
+
97
+ Never derive sequence from count: released ids must remain consumed. Allocation
98
+ checks collisions against attached storage, but saving the real sequence avoids
99
+ searching through old ids. Allocation stops at `Number.MAX_SAFE_INTEGER` instead
100
+ of reusing ids through numeric rounding. The default registry remains in memory;
101
+ injected storage owns retention, indexing, transaction policy, and persistence.
@@ -0,0 +1,63 @@
1
+ # Validate generated quest batches against reviewed facts
2
+
3
+ Run `npx jgengine recipe quest-authoring` for the SDK-typechecked composition.
4
+ Supply your reviewed canonical facts, established quests, catalog ids, external
5
+ unlock sources, and authoring limits. Inject your generator as the async callback;
6
+ the recipe does not choose a model, story, genre, or assets.
7
+
8
+ The generator receives detached, deeply frozen snapshots of the facts, established
9
+ definitions, and limits for each bounded batch. Mutating caller inputs while the
10
+ generator awaits cannot change the reviewed revision or reference lookups. It returns only new quest definitions. Decode untrusted JSON
11
+ into `QuestDef` before returning it; this validator checks authored structure, not
12
+ arbitrary JSON shape. The recipe rejects oversized quest/objective/reference
13
+ batches before structural validation. Keep the existing catalog bounded too;
14
+ partition larger worlds into reviewed dependency-complete catalogs.
15
+
16
+ ```ts
17
+ import { validateQuestCatalog } from "@jgengine/core/game/questCatalog";
18
+ import type { QuestDef } from "@jgengine/core/game/quest";
19
+
20
+ function reviewBatch(existing: readonly QuestDef[], generated: readonly QuestDef[]) {
21
+ return validateQuestCatalog([...existing, ...generated], {
22
+ externalUnlocks: ["caller-declared-world-entry"],
23
+ });
24
+ }
25
+ ```
26
+
27
+ The CLI recipe returns `factsRevision`, `candidate`, `diagnostics`, and `repair`.
28
+ `candidate` is null when structural errors exist. Each issue includes a JSON
29
+ Pointer into the combined catalog, such as `/4/rewards/quests/0`, plus a stable
30
+ code, severity, and referenced id when relevant. Send `repair` with the same
31
+ canonical facts and definitions to your generator for a bounded correction;
32
+ correct the identified fields and validate the whole candidate again. Never
33
+ replace reviewed facts with a model's remembered version. The recipe leaves all
34
+ warnings visible for review before registering the candidate with the journal.
35
+
36
+ Declare unlock ids that other game systems may grant in `externalUnlocks`.
37
+ Unlocks produced by catalog quest rewards are recognized automatically.
38
+ `externallyStartedQuests` declares catalog quests your game may grant active,
39
+ bypassing acceptance prerequisites. These declarations describe possible entry
40
+ points, without asserting when they happen. A requirement may be fulfilled by
41
+ completion of that quest id **or** an unlock with the same id; every requirement
42
+ must be met. Any eligible quest may be accepted directly, so a quest does not
43
+ need an incoming reward follow-up to be reachable.
44
+
45
+ Blocked prerequisites and cycles are warnings under this potential-progression
46
+ model. Objectives are assumed completable; external grants, custom acceptance
47
+ rules, or unavailable objectives can change actual play. Unknown requirement and
48
+ follow-up ids are errors: declare legitimate external unlock ids rather than
49
+ silencing missing references. Use `hasReference` for item, target, inventory, and
50
+ currency namespaces. Its objective context lets a game route custom objective
51
+ kinds to different catalogs. Omitted lookup policy leaves those ids unchecked.
52
+
53
+ Objective and item counts may be fractional or zero; zero can mean an already
54
+ complete objective or a no-op reward. Negative/nonfinite counts and nonfinite
55
+ reward amounts reject. Discrete quantity rules, XP loss, economy balance, story
56
+ contradictions, character motivation, chronology, and consequences remain
57
+ caller-owned review. Play representative paths before treating a valid catalog
58
+ as a coherent world.
59
+
60
+ For item supply and skill gates outside the quest journal, compose the
61
+ [content validation](../reference-content-validation.md) checks. Declare source
62
+ rules explicitly rather than assuming every defined item is obtainable or that
63
+ catalog order proves an item reward can precede a collection objective.
@@ -0,0 +1,50 @@
1
+ # Place shaped inventory items
2
+
3
+ Use `@jgengine/core/inventory/shapedGrid` for discrete cell footprints, including
4
+ rectangles and irregular shapes. Item definitions, values, authored dimensions,
5
+ save schemas, drag controls, and presentation stay in the game.
6
+
7
+ ```ts
8
+ import {
9
+ createShapedGrid,
10
+ findShapedPlacement,
11
+ moveShaped,
12
+ placeShaped,
13
+ type ShapedItem,
14
+ } from "@jgengine/core/inventory/shapedGrid";
15
+
16
+ const item: ShapedItem<{ name: string }> = {
17
+ id: "probe-7",
18
+ value: { name: "Survey probe" },
19
+ footprint: [[0, 0], [0, 1], [1, 1]],
20
+ };
21
+ const board = createShapedGrid<{ name: string }>(4, 3);
22
+ const fit = findShapedPlacement(board, item.footprint, {
23
+ rotations: [0, 1],
24
+ maxChecks: 24,
25
+ });
26
+ if (fit.status === "found") {
27
+ const placed = placeShaped(board, item, fit.origin, fit.rotation);
28
+ if (placed.status === "ok") moveShaped(placed.grid, item.id, [1, 0], 1);
29
+ }
30
+ ```
31
+
32
+ Rotations are quarter turns. Search visits the supplied rotation order, then
33
+ rows and columns; the default rotation order is `[0, 1, 2, 3]`. Coordinates and
34
+ dimensions must be finite safe integers; dimensions are positive and footprints
35
+ are nonempty. Footprints normalize their minimum cell to `[0, 0]` after rotation.
36
+ `canPlace` reports invalid cells before bounds and overlap checks. Pass `ignoreId`
37
+ to either check or search when repositioning an existing item.
38
+
39
+ Search indexes current occupancy once and limits candidate origins to
40
+ `maxChecks` (default 4096). `budget-exceeded` means the search is incomplete;
41
+ `no-space` means every requested origin and rotation was checked. Do not report
42
+ a full bag for an exhausted budget. Indexing and footprint checks scale with
43
+ the caller's board data; the budget does not limit their cell count.
44
+
45
+ Placement operations return new board data. Serialize that data in the caller's
46
+ save format; the engine does not keep a parallel bag or impose item nouns.
47
+ Deepward's prepared adopter maps its existing rectangular item sizes and saved
48
+ `x`, `y`, `rotated` fields into this contract, preserving its `[0, 1]` rotation
49
+ preference and row-major packing. Its UI and extraction rules remain local.
50
+ The prepared patch waits for a coordinator-verified containing package release.
@@ -0,0 +1,74 @@
1
+ # Validate references and declared progression
2
+
3
+ Run these pure validators during authoring, build or CI, never per frame. Decode
4
+ external JSON into typed definitions first. Preserve authored array order and
5
+ stable ids so repeated validation returns identical located diagnostics. Each
6
+ `ContentIssue` carries `contentId`, a caller document `path`, and a `repair` action.
7
+ For a bounded generated batch, validate its dependency-complete reviewed catalog,
8
+ repair the located fields, then validate again before registration.
9
+
10
+ `validateContentReferences` (`@jgengine/core/game/contentValidation`) indexes
11
+ `{ kind, id, path }` definitions once and checks located
12
+ `{ kind, id, path, contentId }` references. A namespace can describe a game-owned
13
+ role: referencing a `questgiver` checks the role rather than merely the existence
14
+ of an NPC. Do not derive roles from names or parse dialogue prose. Validate explicit
15
+ dialogue command arguments against the appropriate namespace.
16
+ For relationships stricter than existence, set a reference's `allowedIds` from
17
+ the game's declared rule, such as this giver's quests or this record's key.
18
+ An existing but disallowed id reports `inconsistent-reference`; omitted policy
19
+ does not infer restrictions from names, prose or ordering.
20
+
21
+ `validateRecipeCatalog` (`@jgengine/core/crafting/recipeCatalog`) checks native
22
+ `RecipeDef` rows and optional item, station and unlock lookups. Build lookup Sets
23
+ once in the adapter. Empty outputs are valid for benefits applied by game code.
24
+ Zero and fractional counts are valid; enforce discrete units in game policy.
25
+ Duplicate input rows reject because crafting checks each row separately; merge
26
+ their counts. Prices, rarity, ingredient scarcity and recipe cycles are game-owned.
27
+ Compose existing `validateQuestCatalog` and `validateDialogueGraph` for their native
28
+ structures; do not replace the quest journal or conversation model.
29
+
30
+ ```ts
31
+ import { validateContentProgression } from "@jgengine/core/game/contentValidation";
32
+
33
+ const result = validateContentProgression({
34
+ initial: ["material:wire", "material:seals", "place:home"],
35
+ rules: [{
36
+ id: "sealed-tank",
37
+ path: "/refits/tank",
38
+ requires: ["material:wire", "material:seals", "place:home"],
39
+ provides: ["benefit:sealed-tank"],
40
+ }],
41
+ required: [{ id: "benefit:sealed-tank", contentId: "tank", path: "/refits/tank" }],
42
+ closedWorld: true,
43
+ });
44
+ ```
45
+
46
+ Facts are opaque game-owned strings. All facts in one rule are necessary;
47
+ alternative sources are separate rules. An initial fact or a reachable source
48
+ can seed an intentional cycle. Missing seeds block cycles; cycles themselves
49
+ are never forbidden. `closedWorld: true` asserts all possible sources have been
50
+ declared and makes blocked rules/required facts errors. The default leaves them
51
+ warnings. Legitimate external suppliers belong in `initial` or a source rule,
52
+ not a blanket declaration of every catalog item as available.
53
+
54
+ Translate skill gates into milestone facts in the game adapter. A repeatable
55
+ action requiring skill 0 and training to 50 can provide milestones through 50;
56
+ it cannot provide the next source's skill-100 fact. Derive milestones from actual
57
+ gates and training caps rather than inventing thousands of skill nodes. Bound the
58
+ analysis to the reviewed content and dependency edges. The engine visits each
59
+ rule and dependency edge with an iterative queue rather than rescanning a catalog
60
+ until convergence.
61
+
62
+ This proves potential availability only. It does not prove affordable quantities,
63
+ repeatability, inventory capacity, consumed prerequisites, exclusive choices,
64
+ chronology, station proximity, story coherence or obtainable rewards in actual play.
65
+ Declare distinct route/branch facts when combining branches would be misleading,
66
+ and drive representative choices through the actual commands with save/reload.
67
+ Keep story facts, terms, assets and economy decisions in the game. Remove older
68
+ generic reference/topology algorithms when adopting these checks; retain custom
69
+ semantic and durable-save policy validators.
70
+
71
+ Verify built tarball imports and the actual adopter in an isolated scratch copy.
72
+ Keep the candidate on a prepared branch until a verified published package contains
73
+ these APIs, then adopt through the games workspace catalog and lockfile together.
74
+ Never route game imports to engine source or add per-game version pins.