@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
@@ -2,6 +2,23 @@
2
2
 
3
3
  Single public authoring path: `defineGame({ systems, loop?, … })` from `@jgengine/shell/defineGame`. Systems are meaningful capabilities — not micro-ticks.
4
4
 
5
+ ## Authoritative state
6
+
7
+ Use `ctx.state()` / `ctx.restore(state)` for detached whole-world persistence, including economy, clock, movement pose, possession, progression and registered system saves. These work without `persist` or a save backend. `ctx.snapshot()` / `ctx.hydrate()` remain client replication and omit save-only modules. `createRuntimeSave({ target: ctx, backend })` selects the authoritative pair automatically; existing snapshot-only targets still work.
8
+
9
+ Restore after initialization has registered every system. Missing keys in older saves retain initialized values; unavailable historical economy/progression cannot be recovered by the engine. Keep a game's legacy migration until its older saves have been converted. Simulation tick and pose-buffer state persist, but callbacks, timers and game-owned closures must be registered/reconstructed at boot or exposed through a system save module.
10
+
11
+ Declare `persist: { version, migrate(data, fromVersion) }` to convert incompatible
12
+ older whole-world saves before restoration. Raw payloads use version 0; matching
13
+ versions bypass the hook. Throw to reject a missing historical runtime module:
14
+ load returns false, leaving the initialized world and stored
15
+ save untouched. A version number alone preserves compatible mismatch loading;
16
+ it is not a rejection gate. Migration exceptions retain the save store's existing
17
+ decode fallback to initial data with idle status; they are not backend errors.
18
+ Explicit context `save` options override `persist`.
19
+
20
+ `ctx.sim.advance(realDt, (stepDt, tick, gameDt) => ...)` owns advancement of `ctx.time` once per simulation step. Use the third argument for scaled gameplay time and `stepDt` for movement; remove manual `ctx.time.advance` calls inside simulation callbacks. Movement prediction opts out with `{ advanceTime: false }` and must not run authoritative gameplay stages. Standalone clocks retain their explicit `advance` API.
21
+
5
22
  ## Portable XP and leveling
6
23
 
7
24
  `leveling` (`@jgengine/core/game/progression`) is usable without a scheduled
@@ -11,6 +28,36 @@ and level state, then emits every reached level in ascending order. State stays
11
28
  in the caller's save format and round-trips as ordinary JSON. See the
12
29
  [portable XP/leveling recipe](recipes/portable-xp-leveling.md).
13
30
 
31
+ ## Coherent authored content
32
+
33
+ Keep stable ids and canonical facts for characters, factions, places, and world rules in the game's authored data. Record each chapter's prerequisites, consequences, and which facts it may introduce or change. The game's creative pillars, story voice, content palette, and desired player experience come from its author; generated prose, items, art, and assets must serve those constraints. Reuse constrained generation and validation code across games while each game owns its content and visual identity; do not fill catalogs from default genre kits or mass generic content.
34
+
35
+ Generate bounded chapter or catalog batches against those facts and existing definitions. Validate references and dependencies before merging each batch, giving humans and agents precise repair locations early to reduce failed retries and speed up game creation. Preserve reviewed facts across batches instead of asking a model to recreate the world from memory. `QuestDef.requires` accepts completed quest ids or unlock ids, so dependency checks must include the game's declared unlocks rather than treating every requirement as a quest. Check reward quest ids and objective/item references against their owning catalogs too.
36
+
37
+ Use `validateQuestCatalog` (`@jgengine/core/game/questCatalog`) before registering quest batches. Declare unlocks supplied by other systems with `externalUnlocks`, and quest grants that bypass acceptance with `externallyStartedQuests`. Catalog reward unlocks are recognized automatically. `hasReference` optionally checks item, target, inventory, and currency ids against caller catalogs. Duplicate ids, missing references, and invalid quantities are errors; blocked prerequisites and dependency cycles are warnings under a model where any eligible quest may be accepted and its objectives completed. External grants can change actual reachability. Follow the [quest authoring recipe](recipes/quest-authoring.md) and `npx jgengine recipe quest-authoring` for SDK-typechecked bounded generation and repair locations.
38
+
39
+ For repeated authoring and repair, follow the [narrative authoring recipe](recipes/narrative-authoring.md). It freezes a reviewed canon revision, bounds dialogue batches and checks explicit character/fact/event-order claims before an injected semantic reviewer approves. New canon is a separate author decision.
40
+
41
+ Validate dialogue structure before opening a conversation:
42
+
43
+ ```ts
44
+ import { validateDialogueGraph, type DialogueGraph } from "@jgengine/core/game/dialogueGraph";
45
+
46
+ function reviewDialogue(graph: DialogueGraph) {
47
+ const issues = validateDialogueGraph(graph);
48
+ const errors = issues.filter((issue) => issue.severity === "error");
49
+ return { publishable: errors.length === 0, issues };
50
+ }
51
+ ```
52
+
53
+ The validator locates duplicate ids, missing start/choice targets, unreachable nodes, and nodes without a route to an ending in linear time without recursive traversal. Loops with an exit are valid. Review warnings against intended entry points and conversation behavior; unreachable nodes may be entered with `goTo`, and closed loops may be deliberate. Run validation on authored batches, outside the frame loop.
54
+
55
+ Structural validity cannot establish story coherence. Review generated content for contradictions with canonical facts, character motivations, chronology, and meaningful consequences; play representative paths and check that choices support the intended experience. Large item counts need useful distinctions, economy balance, discoverability, and bounded runtime lookup. Counting generated items or passing reference checks alone does not establish those qualities.
56
+
57
+ Quest item rewards use a whole-batch grant before XP, currency, and unlocks. `ctx.game.quest` stages every item against the declared inventories and commits only when all fit, including cumulative capacity in one bag. Capacity/kind/unknown-inventory rejections leave inventories and quest status unchanged, so the player can make room and retry. Inventory commits still notify subscribers synchronously; this does not roll back thrown observer exceptions or arbitrary callback side effects.
58
+
59
+ Custom `createQuestJournal` dependencies and `applyQuestRewards` appliers must supply `grantItems` for multiple item rewards. The callback grants the entire batch or returns a rejection without writes; implement staging or a transaction in the owning store. Legacy `grantItem` remains supported for a single reward. Multiple rewards without `grantItems` reject before any grant. Keep callbacks nonthrowing; external stores own exception handling and durable transaction boundaries.
60
+
14
61
  ## API
15
62
 
16
63
  ```ts
@@ -64,6 +111,78 @@ Default stages: fixed `input → movement → combat → ai → activities → c
64
111
  - **Save / replicate** — system modules register via `ctx.game.registerSave` / `registerReplicate` at install.
65
112
  - **Reset / dispose** — `loop.onReset` / `loop.onDispose` (composed) run system hooks.
66
113
 
114
+ Each context owns its installed schedules and listeners, even when worlds share
115
+ one game definition. Retirement removes listeners, disposes systems in reverse
116
+ install order, then invokes the classic loop's disposal hook (including shell
117
+ environment cleanup). Every cleanup is attempted; the first thrown value is
118
+ rethrown afterward. Reentrant or repeated disposal does not invoke hooks again.
119
+ Retired installed handles ignore tick, reset and manual work. `onInit` after
120
+ retirement starts a fresh schedule/listener lifecycle; `onReset` resets an active
121
+ lifecycle and does not reinstall it. Failed installation retires every entered
122
+ system, including one whose `create` throws, and acquired listeners before
123
+ rethrowing the startup error. The caller still retires the classic loop (the
124
+ shell does so on initialization failure). Save/replication registrations are not
125
+ transactional or withdrawn by disposal; discard a failed context, and use a fresh
126
+ context when rebooting systems with registered modules.
127
+
67
128
  ## Snapshot and restore on stateful handles
68
129
 
69
130
  These handles hand back a plain JSON `snapshot()` that later ticks do not mutate, and take it back with `restore(next)`, so a host, save file or replay can rewind them bit-exactly: `game/lootTable` `createLootRegistry` (registered tables; not JSON when an entry uses `generate`), `game/toasts` `createToastQueue`, `game/vfxInstance` `createVfxInstanceStore` (restore emits `stop` then `upsert` so the renderer follows), `session/roundState` `createRoundState` (`RoundSnapshot` now carries `pendingWinner`), `input/lookChannel` `createLookChannel`, and `input/pointer` `createDragCapture` (`state()` out, `restore(state | null)` in).
131
+
132
+ ## Staffed production
133
+
134
+ `stationOutputRate` (`@jgengine/core/work/staffedStation`) converts a plain staffing
135
+ snapshot to cycles per game-second: `(base + perStat * sum(worker.stat)) * efficiency`.
136
+ Only the first `floor(slots)` workers contribute, in caller order. Empty staffing
137
+ keeps base output; set `base: 0` to require workers. Efficiency defaults to one and
138
+ clamps to 0…1. Negative/nonfinite contributions are zero, nonfinite slots admit no
139
+ workers, and nonfinite efficiency or an overflowing result returns zero.
140
+
141
+ The caller owns unique worker ids, assignment/release policy, stat selection,
142
+ modifiers, happiness, and tuning. Resolve numeric contributions from your stat
143
+ store before writing staffing: one station might read `stats.get("strength")`,
144
+ another `stats.get("agility")` with equipment modifiers. Invalid contributions
145
+ still occupy their ordered slot; the function does not promote overflow workers,
146
+ allocate collections, or mutate saved staffing.
147
+
148
+ ```ts
149
+ import { stationOutputRate, type StaffedStation } from "@jgengine/core/work/staffedStation";
150
+ import { createProductionState, productionBuilding, tickProduction, type ProductionState } from "@jgengine/core/crafting/production";
151
+ import { defineStore } from "@jgengine/core/store/defineStore";
152
+ import { defineSystem } from "@jgengine/core/game/defineSystem";
153
+
154
+ const room = defineStore<{
155
+ staffing: StaffedStation;
156
+ efficiency: number;
157
+ production: ProductionState;
158
+ }>("workshop", () => ({
159
+ staffing: { stationId: "workshop", slots: 2, workers: [] },
160
+ efficiency: 1,
161
+ production: createProductionState(),
162
+ }));
163
+ const recipe = productionBuilding({
164
+ id: "workshop", outputs: [{ itemId: "supplies", count: 1 }], rate: 0,
165
+ });
166
+ export const staffedProduction = defineSystem({
167
+ id: "staffed-production",
168
+ tick: { type: "fixed", rate: 4 },
169
+ update(ctx, dt) {
170
+ room.update(ctx, previous => ({ ...previous,
171
+ production: tickProduction({ ...recipe,
172
+ rate: stationOutputRate(previous.staffing, {
173
+ base: 0, perStat: 0.5, efficiency: previous.efficiency,
174
+ }),
175
+ }, previous.production, { dt }),
176
+ }));
177
+ },
178
+ reset(ctx) { room.clear(ctx); },
179
+ });
180
+ ```
181
+
182
+ Install the system via `defineGame({ systems: [staffedProduction] })`; write the
183
+ caller-resolved worker snapshot with `room.update`. Production input buffers,
184
+ output counts, partial-cycle seconds, staffing, and efficiency remain plain data
185
+ in the same store. `ctx.state()`/`ctx.restore()` save and replace that data; reset
186
+ restores the declared initial room. Persist progress, not just staffing, to avoid
187
+ restarting an in-flight batch. Input reservation and power gating continue to use
188
+ `feedProduction` and `tickProduction({ ...recipe, rate }, state, { dt, powered })`.
@@ -19,7 +19,7 @@ For a *connected* walkthrough that wires several primitives into a running loop,
19
19
  - [recipes/vehicle-feel.md](recipes/vehicle-feel.md) — feel target → handling metrics → physical knobs for a ground vehicle, plus the camera, sound and rumble hooks that hang off its telemetry.
20
20
  - [recipes/flight-feel.md](recipes/flight-feel.md) — an aircraft as physical numbers: surfaces, inertia and engine, with the symptom → knob table for how it flies.
21
21
  - [recipes/controls.md](recipes/controls.md) — actions → layered contexts that rebind live → keyboard, touch and gamepad on the same actions → per-axis feel.
22
- - [recipes/character-animation.md](recipes/character-animation.md) — clips → graph → rig → ground: data-driven state machines, triggers and clip events, root motion, foot IK that keeps feet on slopes, and instanced crowds.
22
+ - [recipes/character-animation.md](recipes/character-animation.md) — clips → graph → rig → ground: modular instance visibility, data-driven state machines, triggers and clip events, root motion, foot IK that keeps feet on slopes, and instanced crowds.
23
23
 
24
24
  ## Canonical workflows
25
25
 
@@ -31,15 +31,19 @@ A world is the place you play in: substrate + laws, via `world()` from `@jgengin
31
31
 
32
32
  1. Load the scene document through the shared authored-scene feature.
33
33
  2. Render objects, paths, terrain, foliage, and markers generically.
34
- 3. Query the same ids/layers for spawns, routes, plots, and interaction.
34
+ 3. Query the same ids/layers for spawns, routes, plots, and interaction. The shell batches compatible static catalog props above eight placements per 24 m cell, preserving chosen models, material groups, shadows, measured colliders and object ids for picking. Interactive/custom props, per-object visual/style overrides, and animated, rigged, composed, transparent or mirrored models keep individual renderers. Runtime compatibility changes return a placement to its individual renderer. Visibility compacts each batch to the current visible placements.
35
35
  4. Collision for generated and studio content comes from its data through `ctx.world.solids`; never hand-place invisible blockers for it (see reference.md, World solids).
36
36
  5. Keep derived caches rebuildable; the document remains authoritative.
37
37
 
38
+ For existing `environment()` consumers, `defineGame` overlays only defined editor sky fields on the legacy sky, retaining settings the editor cannot represent; an explicit `backdrop.sky` remains authoritative.
39
+
40
+ For environment-driven surface appearance, sample only selected targets through `material/appearanceSignals`; wetness already includes shelter. Capture the assigned material's dry baseline, apply explicit wet endpoints, and restore it on unbinding. Keep wind in geometry/simulation consumers and local card backlighting separate from scene exposure. See [reference.md](reference.md#environment-fields-weather-hooks--realm-composition).
41
+
38
42
  ### Movement and interaction
39
43
 
40
44
  Choose input intent, controller/motor, collision/navigation, and camera as separate seams. Commands express game intent; world systems resolve motion. Interaction targets come from bounded spatial/sensor queries, not full-world scans.
41
45
 
42
- Walk feel is data, not code. `movement.feel` sets `groundAcceleration`, `airAcceleration` and `groundFriction` (response rates, 1/s) plus `runMultiplier`, `crouchMultiplier`, jump forgiveness (`jumpBufferMs`, `coyoteMs`) and jump shape (`jumpCutFactor`, `apexGravityScale`, `fallGravityScale`, `landingRecoveryMs`), and `physics` sets `gravity` and `jumpVelocity`. A floaty platformer and a weighty shooter differ only in these numbers; pin them with `measureMovement` (`movement/movementProbe`) in a test.
46
+ Walk feel is data, not code. `movement.feel` sets `groundAcceleration`, `airAcceleration` and `groundFriction` (response rates, 1/s) plus `runMultiplier`, `crouchMultiplier`, jump forgiveness (`jumpBufferMs`, `coyoteMs`) and jump shape (`jumpCutFactor`, `apexGravityScale`, `fallGravityScale`, `landingRecoveryMs`), and `physics` sets `gravity` and `jumpVelocity`. The heightfield and capsule paths share these feel controls; voxel jumping retains its press-frame policy. Crouch follows supported catalog poses, and capsule standing requires headroom. Pin numeric movement behavior with `measureMovement` (`movement/movementProbe`) in a test.
43
47
 
44
48
  ### Stateful placed objects
45
49
 
@@ -49,8 +53,12 @@ Per-instance state belongs on the placement, not in a game-owned map keyed by `i
49
53
 
50
54
  Compose perception, selection, planning/behavior, movement, and lifecycle independently. Inject randomness and scheduling. Use spatial indexes, interest tiers, or bounded candidate sets for scale.
51
55
 
56
+ For chase-to-reach and cooldown effects, attach `pursue({ aggroRadius, reach, leashRange, speed, attack })` as behavior data. The host schedules acquisition and solid-aware movement; `behaviorControl` exposes pause/reset and optional threat. Pursuit progress survives GameContext save/replication without a game-local home/cooldown map. For allies or neutral players, configure `defineGame({ pursuit: { eligible(ctx, selfId, candidateId) { /* bounded caller policy */ } } })`; the predicate receives the current world and constrains nearest, held, explicit and forced targets. Bind the receiving world's policy data before advancing after a cold restore. Omission keeps the all-player default; see the reference for faction composition. Use a decision graph and navigation actions when a route or richer perception policy is required.
57
+
52
58
  NPC decisions are a `decisionGraph` behavior: the graph and its blackboard are data, actions are registered by name, and perception or game systems write facts into `behaviorControl(ctx).blackboard(id)` rather than actions querying the world. Set `thinkInterval` so crowds think a few times a second, not every frame, and release movement or claims in `onAbort`.
53
59
 
60
+ For a bounded local detour over movement collision geometry, use `planSolidRoute` (`nav/solidRoute`) with an indexed `solidObstaclesNear` or `sourceObstaclesNear` query. Handle `no-path` and `budget` separately; retain caller movement, goals, terrain and retry policy. See [reference.md](reference.md#local-collision-routes) for limits.
61
+
54
62
  Route on a polygon nav mesh through one `createNavMeshQuery(mesh)` per mesh; price terrain and gate doors with `areaCosts` via `retune`, and keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.
55
63
 
56
64
  ### Fallback-seam diagnostics
@@ -64,6 +72,7 @@ Render seams fall back to placeholders when content is unauthored: default green
64
72
  - Do not duplicate editor coordinates in runtime state.
65
73
  - Public APIs accept semantic policies and caller data, not renderer mechanics or internal tuning rolls.
66
74
  - Generated environments still need deterministic data assertions; authored environments need scene-document assertions.
75
+ - Bound SFX through `PlayableGame.audio.maxVoices` and game-owned `SoundDef.priority`/`maxVoices`; choose reject or deterministic stealing with `voiceOverflow`. See the spatial audio reference for custom hosts and cleanup.
67
76
  - Audio files and licensing belong to `jgengine-assets`; spatial playback policy belongs here.
68
77
 
69
78