@jgengine/core 0.6.0 → 0.8.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 (330) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +2 -0
  3. package/dist/ai/crowd.d.ts +36 -0
  4. package/dist/ai/crowd.js +213 -0
  5. package/dist/ai/jobBoard.d.ts +49 -0
  6. package/dist/ai/jobBoard.js +155 -0
  7. package/dist/ai/spawnDirector.d.ts +61 -0
  8. package/dist/ai/spawnDirector.js +162 -0
  9. package/dist/ai/threat.d.ts +25 -0
  10. package/dist/ai/threat.js +67 -0
  11. package/dist/anim/easing.d.ts +14 -0
  12. package/dist/anim/easing.js +55 -0
  13. package/dist/audio/audioFalloff.d.ts +38 -0
  14. package/dist/audio/audioFalloff.js +32 -0
  15. package/dist/behaviour/behaviour.d.ts +136 -0
  16. package/dist/behaviour/behaviour.js +388 -0
  17. package/dist/board/laneBoard.d.ts +63 -0
  18. package/dist/board/laneBoard.js +114 -0
  19. package/dist/board/timelineBoard.d.ts +36 -0
  20. package/dist/board/timelineBoard.js +70 -0
  21. package/dist/cards/cardPile.d.ts +64 -0
  22. package/dist/cards/cardPile.js +175 -0
  23. package/dist/cards/modifierPipeline.d.ts +33 -0
  24. package/dist/cards/modifierPipeline.js +48 -0
  25. package/dist/combat/abilityKit.d.ts +47 -0
  26. package/dist/combat/abilityKit.js +139 -0
  27. package/dist/combat/animationState.d.ts +47 -0
  28. package/dist/combat/animationState.js +101 -0
  29. package/dist/combat/attackTags.d.ts +14 -0
  30. package/dist/combat/attackTags.js +26 -0
  31. package/dist/combat/breakMeters.d.ts +32 -0
  32. package/dist/combat/breakMeters.js +51 -0
  33. package/dist/combat/comboString.d.ts +35 -0
  34. package/dist/combat/comboString.js +59 -0
  35. package/dist/combat/death.d.ts +8 -0
  36. package/dist/combat/death.js +5 -2
  37. package/dist/combat/defensiveWindow.d.ts +34 -0
  38. package/dist/combat/defensiveWindow.js +66 -0
  39. package/dist/combat/downed.d.ts +35 -0
  40. package/dist/combat/downed.js +104 -0
  41. package/dist/combat/effects.d.ts +12 -1
  42. package/dist/combat/effects.js +31 -19
  43. package/dist/combat/hitReaction.d.ts +23 -0
  44. package/dist/combat/hitReaction.js +20 -0
  45. package/dist/combat/projectiles.d.ts +42 -5
  46. package/dist/combat/projectiles.js +98 -12
  47. package/dist/combat/resistance.d.ts +13 -0
  48. package/dist/combat/resistance.js +46 -0
  49. package/dist/combat/telegraph.d.ts +31 -0
  50. package/dist/combat/telegraph.js +51 -0
  51. package/dist/commands/commandRegistry.d.ts +1 -1
  52. package/dist/commands/commandRegistry.js +2 -1
  53. package/dist/crafting/crop.d.ts +59 -0
  54. package/dist/crafting/crop.js +181 -0
  55. package/dist/crafting/production.d.ts +64 -0
  56. package/dist/crafting/production.js +130 -0
  57. package/dist/crafting/recipe.d.ts +62 -0
  58. package/dist/crafting/recipe.js +101 -0
  59. package/dist/data/dataSource.d.ts +26 -0
  60. package/dist/data/dataSource.js +93 -0
  61. package/dist/data/devProxy.d.ts +11 -0
  62. package/dist/data/devProxy.js +38 -0
  63. package/dist/data/fetchJson.d.ts +20 -0
  64. package/dist/data/fetchJson.js +43 -0
  65. package/dist/data/jsonDataSource.d.ts +4 -0
  66. package/dist/data/jsonDataSource.js +6 -0
  67. package/dist/devtools/devtools.d.ts +157 -0
  68. package/dist/devtools/devtools.js +484 -0
  69. package/dist/devtools/transformTunables.d.ts +14 -0
  70. package/dist/devtools/transformTunables.js +38 -0
  71. package/dist/economy/sharedWallet.d.ts +28 -0
  72. package/dist/economy/sharedWallet.js +62 -0
  73. package/dist/economy/techTree.d.ts +40 -0
  74. package/dist/economy/techTree.js +66 -0
  75. package/dist/game/chat.d.ts +66 -0
  76. package/dist/game/chat.js +165 -0
  77. package/dist/game/cosmetics.d.ts +25 -0
  78. package/dist/game/cosmetics.js +51 -0
  79. package/dist/game/defineGame.d.ts +10 -2
  80. package/dist/game/defineGame.js +2 -1
  81. package/dist/game/events.d.ts +87 -0
  82. package/dist/game/levelSequence.d.ts +44 -0
  83. package/dist/game/levelSequence.js +78 -0
  84. package/dist/game/lootFilter.d.ts +33 -0
  85. package/dist/game/lootFilter.js +48 -0
  86. package/dist/game/ping.d.ts +67 -0
  87. package/dist/game/ping.js +86 -0
  88. package/dist/game/playableGame.d.ts +516 -8
  89. package/dist/game/playableGame.js +6 -1
  90. package/dist/game/progression.d.ts +1 -0
  91. package/dist/game/progression.js +23 -2
  92. package/dist/game/race.d.ts +100 -0
  93. package/dist/game/race.js +236 -0
  94. package/dist/game/runDraft.d.ts +34 -0
  95. package/dist/game/runDraft.js +97 -0
  96. package/dist/game/social.d.ts +69 -0
  97. package/dist/game/social.js +138 -0
  98. package/dist/game/spawnPoints.d.ts +19 -0
  99. package/dist/game/spawnPoints.js +23 -0
  100. package/dist/game/worldItem.d.ts +95 -0
  101. package/dist/game/worldItem.js +138 -0
  102. package/dist/input/actionBindings.d.ts +10 -0
  103. package/dist/input/actionBindings.js +14 -0
  104. package/dist/input/axisInput.d.ts +43 -0
  105. package/dist/input/axisInput.js +70 -0
  106. package/dist/input/gestureSurface.d.ts +31 -0
  107. package/dist/input/gestureSurface.js +120 -0
  108. package/dist/input/pointer.d.ts +69 -0
  109. package/dist/input/pointer.js +65 -0
  110. package/dist/input/touchScheme.d.ts +83 -0
  111. package/dist/input/touchScheme.js +115 -0
  112. package/dist/interaction/contextMenu.d.ts +30 -0
  113. package/dist/interaction/contextMenu.js +22 -0
  114. package/dist/interaction/qte.d.ts +20 -0
  115. package/dist/interaction/qte.js +24 -0
  116. package/dist/interaction/skillCheck.d.ts +20 -0
  117. package/dist/interaction/skillCheck.js +26 -0
  118. package/dist/inventory/shapedGrid.d.ts +56 -0
  119. package/dist/inventory/shapedGrid.js +176 -0
  120. package/dist/inventory/storageTier.d.ts +52 -0
  121. package/dist/inventory/storageTier.js +77 -0
  122. package/dist/item/affix.d.ts +54 -0
  123. package/dist/item/affix.js +115 -0
  124. package/dist/item/durability.d.ts +51 -0
  125. package/dist/item/durability.js +78 -0
  126. package/dist/item/modularItem.d.ts +46 -0
  127. package/dist/item/modularItem.js +75 -0
  128. package/dist/meta/changelog.d.ts +1 -1
  129. package/dist/meta/changelog.js +71 -1
  130. package/dist/movement/dash.d.ts +37 -0
  131. package/dist/movement/dash.js +65 -0
  132. package/dist/movement/movementModel.d.ts +36 -3
  133. package/dist/movement/movementModel.js +81 -5
  134. package/dist/movement/voxelController.d.ts +61 -0
  135. package/dist/movement/voxelController.js +229 -0
  136. package/dist/multiplayer/chatContract.d.ts +42 -0
  137. package/dist/multiplayer/chatContract.js +57 -0
  138. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  139. package/dist/multiplayer/combatSnapshot.js +89 -0
  140. package/dist/multiplayer/identity.d.ts +13 -0
  141. package/dist/multiplayer/identity.js +20 -0
  142. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  143. package/dist/multiplayer/lagCompensation.js +154 -0
  144. package/dist/multiplayer/matchmaking.d.ts +33 -0
  145. package/dist/multiplayer/matchmaking.js +60 -0
  146. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  147. package/dist/multiplayer/poseSyncGate.js +13 -1
  148. package/dist/multiplayer/presenceModel.d.ts +3 -0
  149. package/dist/multiplayer/presenceModel.js +15 -1
  150. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  151. package/dist/multiplayer/simultaneousCommit.js +53 -0
  152. package/dist/multiplayer/voiceContract.d.ts +44 -0
  153. package/dist/multiplayer/voiceContract.js +108 -0
  154. package/dist/nav/navConstrain.d.ts +14 -0
  155. package/dist/nav/navConstrain.js +26 -0
  156. package/dist/nav/navFromEnvironment.d.ts +11 -0
  157. package/dist/nav/navFromEnvironment.js +16 -0
  158. package/dist/nav/navGrid.d.ts +56 -0
  159. package/dist/nav/navGrid.js +265 -0
  160. package/dist/nav/pathFollow.d.ts +31 -0
  161. package/dist/nav/pathFollow.js +69 -0
  162. package/dist/physics/ballisticSweep.d.ts +18 -0
  163. package/dist/physics/ballisticSweep.js +49 -0
  164. package/dist/physics/buoyancy.d.ts +49 -0
  165. package/dist/physics/buoyancy.js +112 -0
  166. package/dist/physics/carryable.d.ts +46 -0
  167. package/dist/physics/carryable.js +110 -0
  168. package/dist/physics/damageZones.d.ts +56 -0
  169. package/dist/physics/damageZones.js +107 -0
  170. package/dist/physics/forceVolume.d.ts +48 -0
  171. package/dist/physics/forceVolume.js +119 -0
  172. package/dist/physics/physicsWorld.d.ts +124 -2
  173. package/dist/physics/physicsWorld.js +493 -41
  174. package/dist/physics/ragdoll.d.ts +49 -0
  175. package/dist/physics/ragdoll.js +103 -0
  176. package/dist/physics/spatialGrid.d.ts +41 -0
  177. package/dist/physics/spatialGrid.js +143 -0
  178. package/dist/physics/structure.d.ts +83 -0
  179. package/dist/physics/structure.js +0 -0
  180. package/dist/physics/traversal.d.ts +75 -0
  181. package/dist/physics/traversal.js +138 -0
  182. package/dist/physics/vehicleBody.d.ts +99 -0
  183. package/dist/physics/vehicleBody.js +233 -0
  184. package/dist/puzzle/cellGrid.d.ts +26 -0
  185. package/dist/puzzle/cellGrid.js +110 -0
  186. package/dist/puzzle/fallingPiece.d.ts +24 -0
  187. package/dist/puzzle/fallingPiece.js +51 -0
  188. package/dist/random/nameGen.d.ts +18 -0
  189. package/dist/random/nameGen.js +35 -0
  190. package/dist/random/rng.d.ts +2 -0
  191. package/dist/random/rng.js +23 -0
  192. package/dist/runtime/adapter.d.ts +35 -0
  193. package/dist/runtime/adapter.js +37 -1
  194. package/dist/runtime/cameraDirector.d.ts +10 -0
  195. package/dist/runtime/cameraDirector.js +25 -0
  196. package/dist/runtime/gameContext.d.ts +107 -3
  197. package/dist/runtime/gameContext.js +253 -7
  198. package/dist/runtime/hostPersistence.d.ts +39 -0
  199. package/dist/runtime/hostPersistence.js +54 -0
  200. package/dist/runtime/inputSnapshot.d.ts +7 -0
  201. package/dist/runtime/inputSnapshot.js +12 -0
  202. package/dist/runtime/motionIntents.d.ts +25 -0
  203. package/dist/runtime/motionIntents.js +25 -0
  204. package/dist/runtime/persistenceScope.d.ts +32 -0
  205. package/dist/runtime/persistenceScope.js +64 -0
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/scene/autoTarget.d.ts +15 -0
  208. package/dist/scene/autoTarget.js +54 -0
  209. package/dist/scene/behaviors.d.ts +13 -1
  210. package/dist/scene/behaviors.js +3 -0
  211. package/dist/scene/captureCheck.d.ts +7 -0
  212. package/dist/scene/captureCheck.js +11 -0
  213. package/dist/scene/entityStore.d.ts +9 -1
  214. package/dist/scene/entityStore.js +25 -2
  215. package/dist/scene/form.d.ts +40 -0
  216. package/dist/scene/form.js +56 -0
  217. package/dist/scene/mount.d.ts +62 -0
  218. package/dist/scene/mount.js +107 -0
  219. package/dist/scene/movementSpeed.d.ts +35 -0
  220. package/dist/scene/movementSpeed.js +20 -0
  221. package/dist/scene/objectQuery.d.ts +18 -0
  222. package/dist/scene/objectQuery.js +108 -0
  223. package/dist/scene/objectStore.d.ts +11 -0
  224. package/dist/scene/objectStore.js +109 -4
  225. package/dist/scene/paintLayer.d.ts +15 -0
  226. package/dist/scene/paintLayer.js +53 -0
  227. package/dist/scene/possession.d.ts +30 -0
  228. package/dist/scene/possession.js +51 -0
  229. package/dist/scene/roster.d.ts +25 -0
  230. package/dist/scene/roster.js +57 -0
  231. package/dist/scene/selection.d.ts +30 -0
  232. package/dist/scene/selection.js +38 -0
  233. package/dist/scene/spatial.d.ts +21 -0
  234. package/dist/scene/spatial.js +63 -1
  235. package/dist/scene/stationClaim.d.ts +43 -0
  236. package/dist/scene/stationClaim.js +109 -0
  237. package/dist/sensor/concealment.d.ts +21 -0
  238. package/dist/sensor/concealment.js +63 -0
  239. package/dist/sensor/freezeMonitor.d.ts +17 -0
  240. package/dist/sensor/freezeMonitor.js +35 -0
  241. package/dist/sensor/frustumSensor.d.ts +60 -0
  242. package/dist/sensor/frustumSensor.js +104 -0
  243. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  244. package/dist/sensor/hiddenStateProbe.js +40 -0
  245. package/dist/sensor/recordingBuffer.d.ts +29 -0
  246. package/dist/sensor/recordingBuffer.js +61 -0
  247. package/dist/sensor/revealQuery.d.ts +22 -0
  248. package/dist/sensor/revealQuery.js +36 -0
  249. package/dist/session/contestedChannel.d.ts +36 -0
  250. package/dist/session/contestedChannel.js +100 -0
  251. package/dist/session/extraction.d.ts +54 -0
  252. package/dist/session/extraction.js +108 -0
  253. package/dist/session/ring.d.ts +36 -0
  254. package/dist/session/ring.js +88 -0
  255. package/dist/session/roles.d.ts +6 -0
  256. package/dist/session/roles.js +32 -0
  257. package/dist/session/roundState.d.ts +79 -0
  258. package/dist/session/roundState.js +158 -0
  259. package/dist/stats/accumulatorMeter.d.ts +31 -0
  260. package/dist/stats/accumulatorMeter.js +81 -0
  261. package/dist/stats/eventMeter.d.ts +34 -0
  262. package/dist/stats/eventMeter.js +76 -0
  263. package/dist/stats/rollCheck.d.ts +15 -0
  264. package/dist/stats/rollCheck.js +15 -0
  265. package/dist/survival/decayMeter.d.ts +59 -0
  266. package/dist/survival/decayMeter.js +95 -0
  267. package/dist/survival/moodle.d.ts +48 -0
  268. package/dist/survival/moodle.js +91 -0
  269. package/dist/survival/regionHealth.d.ts +72 -0
  270. package/dist/survival/regionHealth.js +129 -0
  271. package/dist/tactics/fallingGrid.d.ts +62 -0
  272. package/dist/tactics/fallingGrid.js +158 -0
  273. package/dist/tactics/predictiveQuery.d.ts +34 -0
  274. package/dist/tactics/predictiveQuery.js +44 -0
  275. package/dist/tactics/snapshot.d.ts +18 -0
  276. package/dist/tactics/snapshot.js +65 -0
  277. package/dist/tactics/surface.d.ts +52 -0
  278. package/dist/tactics/surface.js +135 -0
  279. package/dist/tactics/tacticalGrid.d.ts +68 -0
  280. package/dist/tactics/tacticalGrid.js +261 -0
  281. package/dist/time/beatClock.d.ts +40 -0
  282. package/dist/time/beatClock.js +73 -0
  283. package/dist/time/simClock.d.ts +25 -1
  284. package/dist/time/simClock.js +28 -3
  285. package/dist/turn/commit.d.ts +27 -0
  286. package/dist/turn/commit.js +72 -0
  287. package/dist/turn/intent.d.ts +14 -0
  288. package/dist/turn/intent.js +29 -0
  289. package/dist/turn/turnLoop.d.ts +60 -0
  290. package/dist/turn/turnLoop.js +223 -0
  291. package/dist/world/buildPermissions.d.ts +47 -0
  292. package/dist/world/buildPermissions.js +134 -0
  293. package/dist/world/carve.d.ts +105 -0
  294. package/dist/world/carve.js +175 -0
  295. package/dist/world/connectors.d.ts +40 -0
  296. package/dist/world/connectors.js +67 -0
  297. package/dist/world/envField.d.ts +71 -0
  298. package/dist/world/envField.js +96 -0
  299. package/dist/world/environmentSummary.d.ts +53 -0
  300. package/dist/world/environmentSummary.js +129 -0
  301. package/dist/world/features.d.ts +87 -6
  302. package/dist/world/features.js +52 -2
  303. package/dist/world/fog.d.ts +40 -0
  304. package/dist/world/fog.js +137 -0
  305. package/dist/world/gridInstances.d.ts +13 -0
  306. package/dist/world/gridInstances.js +22 -0
  307. package/dist/world/markers.d.ts +52 -0
  308. package/dist/world/markers.js +96 -0
  309. package/dist/world/minimap.d.ts +42 -0
  310. package/dist/world/minimap.js +72 -0
  311. package/dist/world/placedStructureStore.d.ts +39 -0
  312. package/dist/world/placedStructureStore.js +95 -0
  313. package/dist/world/placementController.d.ts +69 -0
  314. package/dist/world/placementController.js +153 -0
  315. package/dist/world/realm.d.ts +61 -0
  316. package/dist/world/realm.js +66 -0
  317. package/dist/world/support.d.ts +27 -0
  318. package/dist/world/support.js +75 -0
  319. package/dist/world/terraform.d.ts +54 -0
  320. package/dist/world/terraform.js +186 -0
  321. package/dist/world/terrain.d.ts +23 -1
  322. package/dist/world/terrain.js +64 -1
  323. package/dist/world/voxelField.d.ts +54 -0
  324. package/dist/world/voxelField.js +241 -0
  325. package/dist/world/walls.d.ts +61 -0
  326. package/dist/world/walls.js +221 -0
  327. package/dist/world/weather.d.ts +92 -0
  328. package/dist/world/weather.js +169 -0
  329. package/llms.txt +4102 -0
  330. package/package.json +3 -2
package/llms.txt ADDED
@@ -0,0 +1,4102 @@
1
+ # @jgengine/core
2
+ > Genre-agnostic TypeScript game engine SDK: game runtime, state store, entity scene, combat, movement, inventory, economy, quests, multiplayer primitives.
3
+
4
+ Version: 0.8.0
5
+ License: AGPL-3.0-only
6
+ Repository: https://github.com/Noisemaker111/jgengine
7
+ Docs: https://jgengine.com
8
+ Imports use deep paths: `@jgengine/core/<path>`.
9
+
10
+ ## Exported surface
11
+
12
+ ### @jgengine/core/ai/crowd
13
+
14
+ - computeFlowField (function): function computeFlowField(grid: NavGrid, goals: readonly NavPoint[], options: FlowFieldOptions = {}): FlowField
15
+ - createCrowdField (function): function createCrowdField(grid: NavGrid): CrowdField
16
+ - selectPoi (function): function selectPoi(pois: readonly Poi[], from: NavPoint, options: SelectPoiOptions): Poi | null
17
+ - FlowFieldOptions (interface): interface FlowFieldOptions
18
+ - FlowField (interface): interface FlowField
19
+ - CrowdField (interface): interface CrowdField
20
+ - Poi (interface): interface Poi
21
+ - SelectPoiOptions (interface): interface SelectPoiOptions
22
+
23
+ ### @jgengine/core/ai/jobBoard
24
+
25
+ - createJobBoard (function): function createJobBoard(): JobBoard
26
+ - JobPhase (type): type JobPhase = "idle" | "travelling" | "working" | "done"
27
+ - JobDef (interface): interface JobDef
28
+ - Job (interface): interface Job
29
+ - JobTickContext (interface): interface JobTickContext
30
+ - JobReport (interface): interface JobReport
31
+ - WorkerState (interface): interface WorkerState
32
+ - JobBoard (interface): interface JobBoard
33
+
34
+ ### @jgengine/core/ai/spawnDirector
35
+
36
+ - createSpawnDirectorState (function): function createSpawnDirectorState(config: SpawnDirectorConfig): SpawnDirectorState
37
+ - raiseAlert (function): function raiseAlert(state: SpawnDirectorState, amount: number): SpawnDirectorState
38
+ - advanceWave (function): function advanceWave(config: SpawnDirectorConfig, state: SpawnDirectorState): SpawnDirectorState
39
+ - advanceSpawnDirector (function): function advanceSpawnDirector(config: SpawnDirectorConfig, state: SpawnDirectorState, dt: number, ctx: DirectorContext): DirectorStep
40
+ - pickSpawnPoint (function): function pickSpawnPoint(points: readonly NavPoint[], players: readonly NavPoint[], options: { roll: number; bias?: number }): NavPoint | null
41
+ - SpawnEntry (interface): interface SpawnEntry
42
+ - WaveManifest (interface): interface WaveManifest
43
+ - SpawnDirectorConfig (interface): interface SpawnDirectorConfig
44
+ - SpawnDirectorState (interface): interface SpawnDirectorState
45
+ - DirectorContext (interface): interface DirectorContext
46
+ - SpawnRequest (interface): interface SpawnRequest
47
+ - DirectorStep (interface): interface DirectorStep
48
+
49
+ ### @jgengine/core/ai/threat
50
+
51
+ - createThreatTable (function): function createThreatTable(config: ThreatTableConfig = {}): ThreatTable
52
+ - ThreatTableConfig (interface): interface ThreatTableConfig
53
+ - ThreatEntry (interface): interface ThreatEntry
54
+ - HighestThreatOptions (interface): interface HighestThreatOptions
55
+ - ThreatTable (interface): interface ThreatTable
56
+
57
+ ### @jgengine/core/anim/easing
58
+
59
+ - lerp (function): function lerp(from: number, to: number, t: number): number
60
+ - clamp01 (function): function clamp01(t: number): number
61
+ - smoothstep (function): function smoothstep(t: number): number
62
+ - easeInQuad (function): function easeInQuad(t: number): number
63
+ - easeOutQuad (function): function easeOutQuad(t: number): number
64
+ - easeInOutQuad (function): function easeInOutQuad(t: number): number
65
+ - easeInCubic (function): function easeInCubic(t: number): number
66
+ - easeOutCubic (function): function easeOutCubic(t: number): number
67
+ - easeInOutCubic (function): function easeInOutCubic(t: number): number
68
+ - easeOutBack (function): function easeOutBack(t: number): number
69
+ - easeOutElastic (function): function easeOutElastic(t: number): number
70
+ - tween (function): function tween(from: number, to: number, t: number, easing: Easing = smoothstep): number
71
+ - timedProgress (function): function timedProgress(startedAt: number, now: number, durationMs: number): number
72
+ - Easing (type): type Easing = (t: number) => number
73
+
74
+ ### @jgengine/core/assets/spriteSheet
75
+
76
+ - sanitizeAssetName (function): function sanitizeAssetName(value: string): string
77
+ - normalizeSpriteSheetGridConfig (function): function normalizeSpriteSheetGridConfig(imageWidth: number, imageHeight: number, config: SpriteSheetGridConfig): Required<SpriteSheetGridConfig>
78
+ - buildSpriteSheetGridBoxes (function): function buildSpriteSheetGridBoxes({ imageWidth, imageHeight, config, namePrefix, names, }: { imageWidth: number; imageHeight: number; config: SpriteSheetGridConfig; namePrefix: string; names?: readonly string[]; }): SpriteSheetSliceBox[]
79
+ - buildSpriteSheetManifest (function): function buildSpriteSheetManifest({ sourceName, imageWidth, imageHeight, config, slices, }: { sourceName: string; imageWidth: number; imageHeight: number; config: SpriteSheetGridConfig; slices: SpriteSheetSliceBox[]; }): SpriteSheetManifest
80
+ - SpriteSheetGridConfig (interface): interface SpriteSheetGridConfig
81
+ - SpriteSheetSliceBox (interface): interface SpriteSheetSliceBox
82
+ - SpriteSheetManifest (interface): interface SpriteSheetManifest
83
+
84
+ ### @jgengine/core/audio/audioFalloff
85
+
86
+ - computeFalloffGain (function): function computeFalloffGain(distance: number, config: AudioFalloffConfig = {}): number
87
+ - resolveEmitterGain (function): function resolveEmitterGain(distance: number, sound: Pick<SoundDef, "gain" | "positional" | "falloff">, busGain: number): number
88
+ - distance3 (function): function distance3(a: { x: number; y: number; z: number }, b: { x: number; y: number; z: number }): number
89
+ - AudioBusId (type): type AudioBusId = string
90
+ - FalloffCurve (type): type FalloffCurve = "linear" | "inverse" | "none"
91
+ - AudioFalloffConfig (interface): interface AudioFalloffConfig
92
+ - AudioBusDef (interface): interface AudioBusDef
93
+ - SoundDef (interface): interface SoundDef
94
+
95
+ ### @jgengine/core/behaviour/behaviour
96
+
97
+ - createBehaviourWorld (function): function createBehaviourWorld(): BehaviourWorld
98
+ - JGEngineRegister (interface): interface JGEngineRegister — Unity-style behaviour lifecycle over a headless id-keyed node tree, ported from vladkrutenyuk/three-start (MIT) and adapted to run without three.js: nodes are plain string ids (use entity instance ids to pair with `entityStore`), and the shell binds render-side hooks separately.
99
+ - RegisterField (type): type RegisterField<TKey extends string, TFallback = unknown> = TKey extends keyof JGEngineRegister ? JGEngineRegister[TKey] : TFallback
100
+ - BehaviourModules (type): type BehaviourModules = RegisterField<"modules", Record<string, BehaviourModule>> — Resolves to the augmented module map when `JGEngineRegister` declares one.
101
+ - Behaviour (class): class Behaviour — Subclass and override the lifecycle hooks. A behaviour only joins the per-frame update dispatch if it actually overrides `onUpdate` (prototype-identity check at each activation), so hook-only behaviours cost nothing per frame.
102
+ - BehaviourModule (class): class BehaviourModule — A world-lifetime service with typed sibling access via `this.modules`. Modules awake and start before any behaviour during `world.start()`, subscribe to update dispatch first (their `onUpdate` fires before every behaviour's), and have no disable/destroy — they live as long as the world.
103
+ - BehaviourWorld (interface): interface BehaviourWorld
104
+
105
+ ### @jgengine/core/board/laneBoard
106
+
107
+ - createLaneBoardState (function): function createLaneBoardState<C>(config: LaneBoardConfig<C>): LaneBoardState<C>
108
+ - placeCard (function): function placeCard<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>, lane: number, side: Side, card: C): LaneResult<C>
109
+ - removeCard (function): function removeCard<C>(state: LaneBoardState<C>, lane: number, side: Side, predicate: (card: C, index: number) => boolean): LaneResult<C>
110
+ - laneAggregate (function): function laneAggregate<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>, lane: number, side: Side): LaneAggregate
111
+ - laneOutcome (function): function laneOutcome<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>, lane: number): LaneOutcome
112
+ - boardTotals (function): function boardTotals<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>): Readonly<Record<Side, number>>
113
+ - lanesWon (function): function lanesWon<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>): Readonly<Record<Side, number>>
114
+ - createLaneBoard (function): function createLaneBoard<C>(config: LaneBoardConfig<C>): LaneBoard<C>
115
+ - Side (type): type Side = string
116
+ - LaneRuleInput (interface): interface LaneRuleInput<C>
117
+ - LaneRule (interface): interface LaneRule<C>
118
+ - LaneBoardConfig (interface): interface LaneBoardConfig<C>
119
+ - LaneCells (interface): interface LaneCells<C>
120
+ - LaneBoardState (interface): interface LaneBoardState<C>
121
+ - LaneAggregate (interface): interface LaneAggregate
122
+ - LaneOutcome (interface): interface LaneOutcome
123
+ - LaneRejection (type): type LaneRejection = "invalid-lane" | "invalid-side"
124
+ - LanePlaceResult (interface): interface LanePlaceResult<C>
125
+ - LanePlaceRejected (interface): interface LanePlaceRejected
126
+ - LaneResult (type): type LaneResult<C> = LanePlaceResult<C> | LanePlaceRejected
127
+ - LaneBoard (interface): interface LaneBoard<C>
128
+
129
+ ### @jgengine/core/board/timelineBoard
130
+
131
+ - createTimelineBoardState (function): function createTimelineBoardState(slots: readonly TimelineSlotConfig[]): TimelineBoardState
132
+ - tickTimeline (function): function tickTimeline(state: TimelineBoardState, dtMs: number): TimelineTickResult
133
+ - setSlotEnabled (function): function setSlotEnabled(state: TimelineBoardState, slotId: string, enabled: boolean): TimelineBoardState
134
+ - resetSlot (function): function resetSlot(state: TimelineBoardState, slotId: string, remainingMs?: number): TimelineBoardState
135
+ - createTimelineBoard (function): function createTimelineBoard(slots: readonly TimelineSlotConfig[]): TimelineBoard
136
+ - TimelineSlotConfig (interface): interface TimelineSlotConfig
137
+ - TimelineSlot (interface): interface TimelineSlot
138
+ - TimelineBoardState (interface): interface TimelineBoardState
139
+ - TimelineFire (interface): interface TimelineFire
140
+ - TimelineTickResult (interface): interface TimelineTickResult
141
+ - TimelineBoard (interface): interface TimelineBoard
142
+
143
+ ### @jgengine/core/cards/cardPile
144
+
145
+ - pileRng (function): function pileRng(seed: string | number): () => number
146
+ - shuffleWithRng (function): function shuffleWithRng<T>(values: readonly T[], rng: () => number): T[]
147
+ - createCardPileState (function): function createCardPileState(config: CardPileConfig, initial?: Partial<Record<ZoneName, readonly string[]>>): CardPileState
148
+ - zoneOf (function): function zoneOf(state: CardPileState, cardId: string): ZoneName | null
149
+ - countIn (function): function countIn(state: CardPileState, zone: ZoneName): number
150
+ - peek (function): function peek(state: CardPileState, zone: ZoneName, n = 1): readonly string[]
151
+ - shuffleZone (function): function shuffleZone(state: CardPileState, zone: ZoneName, seed: string | number): CardPileState
152
+ - moveCards (function): function moveCards(state: CardPileState, ids: readonly string[], from: ZoneName, to: ZoneName, position: "top" | "bottom" = "top"): PileResult
153
+ - draw (function): function draw(state: CardPileState, n: number, options: { from: ZoneName; to: ZoneName; handLimit?: number; reshuffleFrom?: ZoneName; seed?: string | number; }): DrawResult
154
+ - createCardPile (function): function createCardPile(config: CardPileConfig, initial?: Partial<Record<ZoneName, readonly string[]>>): CardPile
155
+ - ZoneName (type): type ZoneName = string
156
+ - CardPileConfig (interface): interface CardPileConfig
157
+ - CardPileState (interface): interface CardPileState
158
+ - DrawResult (interface): interface DrawResult
159
+ - PileRejection (type): type PileRejection = | "unknown-zone" | "card-not-in-zone" | "hand-limit" | "empty-source"
160
+ - PileMoveResult (interface): interface PileMoveResult
161
+ - PileMoveRejected (interface): interface PileMoveRejected
162
+ - PileResult (type): type PileResult = PileMoveResult | PileMoveRejected
163
+ - CardPile (interface): interface CardPile
164
+
165
+ ### @jgengine/core/cards/modifierPipeline
166
+
167
+ - runPipeline (function): function runPipeline<V>(base: V, modifiers: readonly Modifier<V>[], equals: (a: V, b: V) => boolean = Object.is): PipelineResult<V>
168
+ - createModifierPipeline (function): function createModifierPipeline<V>(initial: readonly Modifier<V>[] = [], equals: (a: V, b: V) => boolean = Object.is): ModifierPipeline<V>
169
+ - Modifier (interface): interface Modifier<V>
170
+ - PipelineContext (interface): interface PipelineContext<V>
171
+ - TraceStep (interface): interface TraceStep<V>
172
+ - PipelineResult (interface): interface PipelineResult<V>
173
+ - ModifierPipeline (interface): interface ModifierPipeline<V>
174
+
175
+ ### @jgengine/core/combat/abilityKit
176
+
177
+ - createAbilityKit (function): function createAbilityKit(configs: readonly AbilitySlotConfig[]): AbilityKit
178
+ - AbilityCastType (type): type AbilityCastType = "instant" | "projectile" | "channel" | "targeted"
179
+ - AbilitySlotState (type): type AbilitySlotState = "ready" | "cooldown" | "no-resource" | "just-cast"
180
+ - AbilitySlotConfig (interface): interface AbilitySlotConfig
181
+ - AbilitySlotSnapshot (interface): interface AbilitySlotSnapshot
182
+ - AbilityCastReason (type): type AbilityCastReason = "unknown-slot" | "cooldown" | "no-resource"
183
+ - AbilitySlotRetune (interface): interface AbilitySlotRetune
184
+ - AbilityCastResult (type): type AbilityCastResult = | { ok: true; slot: AbilitySlotSnapshot } | { ok: false; reason: AbilityCastReason; slot: AbilitySlotSnapshot | null }
185
+ - AbilityKit (interface): interface AbilityKit
186
+
187
+ ### @jgengine/core/combat/animationState
188
+
189
+ - clipDurationMs (function): function clipDurationMs(clip: AnimationClip): number
190
+ - frameAtMs (function): function frameAtMs(clip: AnimationClip, elapsedMs: number): number
191
+ - phasesAtFrame (function): function phasesAtFrame(clip: AnimationClip, frame: number): FramePhase[]
192
+ - activeRangeAtFrame (function): function activeRangeAtFrame(clip: AnimationClip, frame: number): FrameRange | null
193
+ - createAnimationState (function): function createAnimationState(config: AnimationStateConfig): AnimationState
194
+ - FramePhase (type): type FramePhase = "windup" | "active" | "recovery" | "cancel"
195
+ - FrameRange (interface): interface FrameRange
196
+ - AnimationClip (interface): interface AnimationClip
197
+ - AnimationSnapshot (interface): interface AnimationSnapshot
198
+ - AnimationTickResult (interface): interface AnimationTickResult
199
+ - AnimationState (interface): interface AnimationState
200
+ - AnimationStateConfig (interface): interface AnimationStateConfig
201
+
202
+ ### @jgengine/core/combat/attackTags
203
+
204
+ - attackMeta (function): function attackMeta(tags: readonly AttackTag[], extra?: Omit<AttackMeta, "tags">): AttackMeta
205
+ - hasTag (function): function hasTag(meta: AttackMeta, tag: AttackTag): boolean
206
+ - hasAnyTag (function): function hasAnyTag(meta: AttackMeta, tags: readonly AttackTag[]): boolean
207
+ - isBlockable (function): function isBlockable(meta: AttackMeta): boolean
208
+ - isParryable (function): function isParryable(meta: AttackMeta): boolean
209
+ - isDodgeable (function): function isDodgeable(meta: AttackMeta): boolean
210
+ - counters (function): function counters(meta: AttackMeta, move: CounterMove): boolean
211
+ - AttackTag (type): type AttackTag = | "unblockable" | "thrust" | "sweep" | "grab" | "overhead" | "ranged" | "aerial"
212
+ - AttackMeta (interface): interface AttackMeta
213
+ - CounterMove (type): type CounterMove = "mikiri" | "deflect" | "sidestep"
214
+
215
+ ### @jgengine/core/combat/breakMeters
216
+
217
+ - createStaggerMeter (function): function createStaggerMeter(config: StaggerMeterConfig): StaggerMeter
218
+ - createBuildupMeter (function): function createBuildupMeter(config: BuildupMeterConfig): BuildupMeter
219
+ - StaggerMeterConfig (interface): interface StaggerMeterConfig
220
+ - StaggerMeter (interface): interface StaggerMeter
221
+ - BuildupMeterConfig (interface): interface BuildupMeterConfig
222
+ - BuildupProc (interface): interface BuildupProc
223
+ - BuildupMeter (interface): interface BuildupMeter
224
+
225
+ ### @jgengine/core/combat/comboString
226
+
227
+ - stepById (function): function stepById(combo: ComboString, stepId: string): ComboStep | null
228
+ - advanceCombo (function): function advanceCombo(input: AdvanceComboInput): AdvanceComboResult
229
+ - createComboRunner (function): function createComboRunner(combo: ComboString, anim: AnimationState): ComboRunner
230
+ - ComboStep (interface): interface ComboStep
231
+ - ComboString (interface): interface ComboString
232
+ - AdvanceComboInput (interface): interface AdvanceComboInput
233
+ - AdvanceComboResult (type): type AdvanceComboResult = | { accepted: true; step: ComboStep } | { accepted: false; reason: "unknown-step" | "not-chainable" | "wrong-stance" | "window-closed" }
234
+ - ComboRunner (interface): interface ComboRunner
235
+
236
+ ### @jgengine/core/combat/death
237
+
238
+ - normalizeOnDeath (function): function normalizeOnDeath(spec: OnDeathSpec | null | undefined): NormalizedOnDeath
239
+ - createDeathSystem (function): function createDeathSystem(deps: DeathSystemDeps): DeathSystem
240
+ - deathReasonFromEffect (function): function deathReasonFromEffect(ctx: EffectDeathContext): DeathReason
241
+ - DeathReasonKind (type): type DeathReasonKind = DeathReason["kind"]
242
+ - OnDeathDropRule (interface): interface OnDeathDropRule
243
+ - OnDeathCommandRule (interface): interface OnDeathCommandRule
244
+ - DropMode (type): type DropMode = "grant" | "world"
245
+ - OnDeathSpec (interface): interface OnDeathSpec
246
+ - NormalizedOnDeath (interface): interface NormalizedOnDeath
247
+ - DeathIdentity (interface): interface DeathIdentity
248
+ - DeathSystemDeps (interface): interface DeathSystemDeps
249
+ - DeathResolution (type): type DeathResolution = | { status: "resolved"; drops: Drop[]; ranCommand: string | null } | { status: "rejected"; reason: string }
250
+ - DeathSystem (interface): interface DeathSystem
251
+ - EffectDeathContext (interface): interface EffectDeathContext
252
+
253
+ ### @jgengine/core/combat/defensiveWindow
254
+
255
+ - windowActiveAt (function): function windowActiveAt(config: DefensiveWindowConfig, elapsedMs: number): boolean
256
+ - iframeActiveAt (function): function iframeActiveAt(config: DefensiveWindowConfig, elapsedMs: number): boolean
257
+ - totalWindowMs (function): function totalWindowMs(config: DefensiveWindowConfig): number
258
+ - resolveDefense (function): function resolveDefense(input: ResolveDefenseInput): DefenseResolution
259
+ - createDefensiveWindow (function): function createDefensiveWindow(config: DefensiveWindowConfig): DefensiveWindow
260
+ - DefenseKind (type): type DefenseKind = "parry" | "block" | "dodge"
261
+ - DefensiveWindowConfig (interface): interface DefensiveWindowConfig
262
+ - DefenseOutcome (type): type DefenseOutcome = "parry" | "block" | "iframe" | "hit"
263
+ - DefenseResolution (interface): interface DefenseResolution
264
+ - ResolveDefenseInput (interface): interface ResolveDefenseInput
265
+ - DefensiveWindow (interface): interface DefensiveWindow
266
+
267
+ ### @jgengine/core/combat/downed
268
+
269
+ - createDownedState (function): function createDownedState(config: DownedConfig): DownedState
270
+ - DownedPhase (type): type DownedPhase = "alive" | "downed" | "dead"
271
+ - DownedConfig (interface): interface DownedConfig
272
+ - DownedEntry (interface): interface DownedEntry
273
+ - DownedEventKind (type): type DownedEventKind = | "downed" | "revived" | "reviving" | "died" | "banner.created" | "banner.expired" | "respawned"
274
+ - DownedEvent (interface): interface DownedEvent
275
+ - DownedState (interface): interface DownedState
276
+
277
+ ### @jgengine/core/combat/effects
278
+
279
+ - resolveAreaTargets (function): function resolveAreaTargets(spatial: CombatSpatialDeps, input: AreaTargetInput, accept?: (instanceId: string) => boolean): AreaTarget[]
280
+ - createEffectSystem (function): function createEffectSystem(deps: EffectSystemDeps): EffectSystem
281
+ - ReceiveRule (interface): interface ReceiveRule
282
+ - ReceiveMap (type): type ReceiveMap = Record<string, ReceiveRule>
283
+ - EffectVia (interface): interface EffectVia
284
+ - SingleTargetEffectInput (interface): interface SingleTargetEffectInput
285
+ - AreaEffectInput (interface): interface AreaEffectInput
286
+ - EffectInput (type): type EffectInput = SingleTargetEffectInput | AreaEffectInput
287
+ - AppliedPoolDelta (interface): interface AppliedPoolDelta
288
+ - EffectResult (interface): interface EffectResult
289
+ - LethalContext (interface): interface LethalContext
290
+ - CombatSpatialDeps (interface): interface CombatSpatialDeps
291
+ - AreaTargetInput (interface): interface AreaTargetInput
292
+ - AreaTarget (interface): interface AreaTarget
293
+ - EffectSystemDeps (interface): interface EffectSystemDeps
294
+ - EffectSystem (interface): interface EffectSystem
295
+
296
+ ### @jgengine/core/combat/hitReaction
297
+
298
+ - resolveHitReaction (function): function resolveHitReaction(config: HitReactionConfig, input: HitReactionInput): HitReaction
299
+ - applyImpulse (function): function applyImpulse(position: EntityPosition, impulse: [number, number, number]): EntityPosition
300
+ - CameraShake (interface): interface CameraShake
301
+ - HitReactionConfig (interface): interface HitReactionConfig
302
+ - HitReactionInput (interface): interface HitReactionInput
303
+ - HitReaction (interface): interface HitReaction
304
+
305
+ ### @jgengine/core/combat/projectiles
306
+
307
+ - createProjectileSystem (function): function createProjectileSystem(deps: ProjectileSystemDeps): ProjectileSystem
308
+ - ProjectileShotInput (interface): interface ProjectileShotInput
309
+ - EntityRaycastHit (interface): interface EntityRaycastHit — A raycast hit against a scene entity — the ray reached `instanceId` at `distance` along its length.
310
+ - ObjectRaycastHit (interface): interface ObjectRaycastHit — A raycast hit against a placed scene object's bounding box — see `ProjectileObjectsDeps.halfExtents`.
311
+ - RaycastHit (type): type RaycastHit = EntityRaycastHit | ObjectRaycastHit
312
+ - Raycast (type): type Raycast = (from: string, aim: Aim, range: number) => RaycastHit[]
313
+ - ProjectileSettleReport (interface): interface ProjectileSettleReport
314
+ - ProjectileObjectsDeps (interface): interface ProjectileObjectsDeps — Read-only scene object access for the default `raycast`; matches `ObjectStore.list()`/a catalog lookup structurally.
315
+ - ProjectileSystemDeps (interface): interface ProjectileSystemDeps
316
+ - ProjectileHit (type): type ProjectileHit = | { kind: "entity"; instanceId: string; distance: number } | { kind: "object"; instanceId: string; catalogId: string; distance: number }
317
+ - ProjectilePrediction (interface): interface ProjectilePrediction
318
+ - SettleResult (type): type SettleResult = | { status: "settled"; shotId: string; at: [number, number, number]; hits: EffectResult[] } | { status: "rejected"; shotId: string; reason: string }
319
+ - ProjectileSystem (interface): interface ProjectileSystem
320
+
321
+ ### @jgengine/core/combat/resistance
322
+
323
+ - resolveResistance (function): function resolveResistance(matrix: ResistanceMatrix, category: string, targetProperties: readonly string[]): ResistanceResult
324
+ - resistanceScale (function): function resistanceScale(matrix: ResistanceMatrix, category: string, targetProperties: readonly string[]): number
325
+ - ResistVerdict (type): type ResistVerdict = "immune" | "resist" | "normal" | "vulnerable"
326
+ - ResistanceMatrix (interface): interface ResistanceMatrix
327
+ - ResistanceResult (interface): interface ResistanceResult
328
+
329
+ ### @jgengine/core/combat/telegraph
330
+
331
+ - telegraphProgress (function): function telegraphProgress(windupMs: number, startedAtMs: number, nowMs: number): number
332
+ - telegraphFired (function): function telegraphFired(windupMs: number, startedAtMs: number, nowMs: number): boolean
333
+ - telegraphTurnProgress (function): function telegraphTurnProgress(config: TelegraphConfig, startedTurn: number, currentTurn: number): number
334
+ - telegraphFiredAtTurn (function): function telegraphFiredAtTurn(config: TelegraphConfig, startedTurn: number, currentTurn: number): boolean
335
+ - telegraphTurnsRemaining (function): function telegraphTurnsRemaining(config: TelegraphConfig, startedTurn: number, currentTurn: number): number
336
+ - pointInTelegraph (function): function pointInTelegraph(config: TelegraphConfig, point: EntityPosition): boolean
337
+ - TelegraphShape (type): type TelegraphShape = | { kind: "circle"; radius: number } | { kind: "ring"; radius: number; innerRadius: number } | { kind: "cone"; radius: number; angle: number } | { kind: "line"; length: number; width: number }
338
+ - TelegraphConfig (interface): interface TelegraphConfig
339
+
340
+ ### @jgengine/core/commands/commandRegistry
341
+
342
+ - createCommandRegistry (function): function createCommandRegistry<TState>(): CommandRegistry<TState>
343
+ - CommandRejection (interface): interface CommandRejection
344
+ - CommandDefinition (interface): interface CommandDefinition<TState, TInput>
345
+ - CommandResult (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string } | { status: "unknown-command" }
346
+ - CommandRegistry (interface): interface CommandRegistry<TState>
347
+
348
+ ### @jgengine/core/crafting/crop
349
+
350
+ - emptyTile (function): function emptyTile(): CropTileState
351
+ - canTill (function): function canTill(state: CropTileState): boolean
352
+ - tillTile (function): function tillTile(state: CropTileState): CropTileState
353
+ - canPlant (function): function canPlant(state: CropTileState): boolean
354
+ - plantCrop (function): function plantCrop(state: CropTileState, cropId: string): CropTileState
355
+ - waterTile (function): function waterTile(state: CropTileState): CropTileState
356
+ - maturityDays (function): function maturityDays(def: CropDef): number
357
+ - advanceCropDay (function): function advanceCropDay(def: CropDef, state: CropTileState): CropTileState
358
+ - harvestCrop (function): function harvestCrop(def: CropDef, state: CropTileState): HarvestResult
359
+ - tileKey (function): function tileKey(coord: TileCoord): string
360
+ - singleTile (function): function singleTile(): TilePattern
361
+ - squarePattern (function): function squarePattern(radius: number): TilePattern
362
+ - diamondPattern (function): function diamondPattern(radius: number): TilePattern
363
+ - rectPattern (function): function rectPattern(width: number, depth: number): TilePattern
364
+ - patternAt (function): function patternAt(center: TileCoord, pattern: TilePattern): TileCoord[]
365
+ - applyToolToTiles (function): function applyToolToTiles(tiles: ReadonlyMap<string, CropTileState>, center: TileCoord, pattern: TilePattern, apply: (state: CropTileState, coord: TileCoord) => CropTileState, fill: () => CropTileState = emptyTile): ApplyToolResult
366
+ - createDayTicker (function): function createDayTicker(startDay = 0): DayTicker
367
+ - createCropField (function): function createCropField(catalog: CropCatalog = () => null): CropField
368
+ - SoilState (type): type SoilState = "untilled" | "tilled"
369
+ - CropDef (interface): interface CropDef
370
+ - CropTileState (interface): interface CropTileState
371
+ - CropCatalog (type): type CropCatalog = (cropId: string) => CropDef | null
372
+ - HarvestResult (interface): interface HarvestResult
373
+ - TileCoord (type): type TileCoord = readonly [number, number]
374
+ - TilePattern (type): type TilePattern = readonly TileCoord[]
375
+ - ApplyToolResult (interface): interface ApplyToolResult
376
+ - DayTicker (interface): interface DayTicker
377
+ - CropField (interface): interface CropField
378
+
379
+ ### @jgengine/core/crafting/production
380
+
381
+ - productionBuilding (function): function productionBuilding(config: ProductionBuildingConfig): ProductionBuildingDef
382
+ - createProductionState (function): function createProductionState(): ProductionState
383
+ - acceptsInput (function): function acceptsInput(def: ProductionBuildingDef, state: ProductionState, itemId: string): boolean
384
+ - feedProduction (function): function feedProduction(def: ProductionBuildingDef, state: ProductionState, itemId: string, count: number): { state: ProductionState; accepted: number }
385
+ - drainOutput (function): function drainOutput(state: ProductionState, itemId: string, count?: number): { state: ProductionState; taken: number }
386
+ - tickProduction (function): function tickProduction(def: ProductionBuildingDef, state: ProductionState, input: ProductionTickInput): ProductionState
387
+ - advanceTransport (function): function advanceTransport(path: TransportPath, items: readonly TransportItem[], dt: number): { items: TransportItem[]; delivered: TransportItem[] }
388
+ - resolvePowerGrid (function): function resolvePowerGrid(supply: number, consumers: readonly PowerConsumer[]): PowerGridResult
389
+ - ProductionBuildingDef (interface): interface ProductionBuildingDef
390
+ - ProductionBuildingConfig (interface): interface ProductionBuildingConfig
391
+ - ItemCounts (type): type ItemCounts = Readonly<Record<string, number>>
392
+ - ProductionState (interface): interface ProductionState
393
+ - ProductionTickInput (interface): interface ProductionTickInput
394
+ - TransportItem (interface): interface TransportItem
395
+ - TransportPath (interface): interface TransportPath
396
+ - PowerConsumer (interface): interface PowerConsumer
397
+ - PowerGridResult (interface): interface PowerGridResult
398
+
399
+ ### @jgengine/core/crafting/recipe
400
+
401
+ - craftSeconds (function): function craftSeconds(recipe: RecipeDef): number
402
+ - missingInputs (function): function missingInputs(state: InventoryState, recipe: RecipeDef): RecipeItem[]
403
+ - hasRecipeInputs (function): function hasRecipeInputs(state: InventoryState, recipe: RecipeDef): boolean
404
+ - stationSatisfied (function): function stationSatisfied(recipe: RecipeDef, context: CraftContext): boolean
405
+ - canCraft (function): function canCraft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context: CraftContext = {}): CraftCheck
406
+ - craft (function): function craft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context: CraftContext = {}): CraftResult
407
+ - createRecipeGraph (function): function createRecipeGraph(defs: readonly RecipeDef[] = []): RecipeGraph
408
+ - RecipeItem (interface): interface RecipeItem
409
+ - RecipeDef (interface): interface RecipeDef
410
+ - StationInstance (interface): interface StationInstance
411
+ - CraftContext (interface): interface CraftContext
412
+ - CraftRejection (type): type CraftRejection = | { reason: "missing-inputs"; missing: readonly RecipeItem[] } | { reason: "no-station"; station: string } | { reason: "locked"; requires: readonly string[] } | { reason: "no-output-space" }
413
+ - CraftCheck (type): type CraftCheck = { ok: true } | ({ ok: false } & CraftRejection)
414
+ - CraftResult (type): type CraftResult = { status: "ok"; state: InventoryState } | ({ status: "rejected" } & CraftRejection)
415
+ - RecipeGraph (interface): interface RecipeGraph
416
+
417
+ ### @jgengine/core/data/dataSource
418
+
419
+ - createDataSource (function): function createDataSource<T>(load: (signal: AbortSignal) => Promise<T>, options: DataSourceOptions = {}): DataSource<T>
420
+ - DataSourceStatus (type): type DataSourceStatus = "idle" | "loading" | "ready" | "error"
421
+ - DataSourceState (interface): interface DataSourceState<T>
422
+ - DataSourceClock (interface): interface DataSourceClock
423
+ - DataSourceOptions (interface): interface DataSourceOptions
424
+ - RefreshOptions (interface): interface RefreshOptions
425
+ - DataSource (interface): interface DataSource<T>
426
+
427
+ ### @jgengine/core/data/devProxy
428
+
429
+ - parseDevProxyTable (function): function parseDevProxyTable(raw: string | undefined): DevProxyTable
430
+ - proxiedUrl (function): function proxiedUrl(target: string, options: ProxiedUrlOptions = {}): string
431
+ - DevProxyTable (interface): interface DevProxyTable
432
+ - ProxiedUrlOptions (interface): interface ProxiedUrlOptions
433
+ - DEFAULT_DEV_PROXY_PREFIX (const): const DEFAULT_DEV_PROXY_PREFIX: "/proxy"
434
+
435
+ ### @jgengine/core/data/fetchJson
436
+
437
+ - fetchJson (function): function fetchJson<T>(url: string, options: FetchJsonOptions = {}): Promise<T>
438
+ - FetchImpl (type): type FetchImpl = typeof fetch
439
+ - FetchJsonOptions (interface): interface FetchJsonOptions
440
+ - HttpStatusError (class): class HttpStatusError extends Error
441
+ - JsonParseError (class): class JsonParseError extends Error
442
+
443
+ ### @jgengine/core/data/jsonDataSource
444
+
445
+ - createJsonDataSource (function): function createJsonDataSource<T>(url: string, options: JsonDataSourceOptions = {}): DataSource<T>
446
+ - JsonDataSourceOptions (type): type JsonDataSourceOptions = DataSourceOptions & Omit<FetchJsonOptions, "signal">
447
+
448
+ ### @jgengine/core/devtools/devtools
449
+
450
+ - formatLogMessage (function): function formatLogMessage(args: readonly unknown[]): string
451
+ - createDevtools (function): function createDevtools(): Devtools
452
+ - tunable (function): function tunable<T>(name: string, initial: T, options?: TunableOptions<T>): Tunable<T>
453
+ - snapshotDevtools (function): function snapshotDevtools(): DevtoolsSnapshot
454
+ - instrumentLatency (function): function instrumentLatency<T extends object>(target: T, methods: readonly (keyof T)[], record: (ms: number) => void = devtools.latency.record): T
455
+ - DevtoolsControlKind (type): type DevtoolsControlKind = "slider" | "toggle" | "color" | "select" | "text"
456
+ - TunableOptions (interface): interface TunableOptions<T>
457
+ - DevtoolsControl (interface): interface DevtoolsControl
458
+ - Tunable (interface): interface Tunable<T>
459
+ - FrameStats (interface): interface FrameStats
460
+ - RenderSample (interface): interface RenderSample
461
+ - LatencyStats (interface): interface LatencyStats
462
+ - DevtoolsLogLevel (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error"
463
+ - DevtoolsLogEntry (interface): interface DevtoolsLogEntry
464
+ - DiscoveredEntry (interface): interface DiscoveredEntry
465
+ - TunableAccessor (interface): interface TunableAccessor
466
+ - DevtoolsOverrides (interface): interface DevtoolsOverrides
467
+ - DevtoolsSnapshot (interface): interface DevtoolsSnapshot
468
+ - Devtools (interface): interface Devtools
469
+ - devtools (const): const devtools: Devtools
470
+
471
+ ### @jgengine/core/devtools/transformTunables
472
+
473
+ - transformTunableExports (function): function transformTunableExports(code: string, table: string): TunableTransformResult
474
+ - tunableModuleTable (function): function tunableModuleTable(id: string): string | null
475
+ - tunableDiscoveryPlugin (function): function tunableDiscoveryPlugin(): { name: string; enforce: "pre"; transform(code: string, id: string): { code: string; map: null } | null; }
476
+ - TunableTransformResult (interface): interface TunableTransformResult
477
+
478
+ ### @jgengine/core/economy/currency
479
+
480
+ - sanitizeCurrencyAmount (function): function sanitizeCurrencyAmount(amount: number): number
481
+ - formatCurrencyAmount (function): function formatCurrencyAmount(currency: CurrencyDefinition, amount: number): string
482
+ - insufficientCurrencyReason (function): function insufficientCurrencyReason(currency: CurrencyDefinition, needed: number, current: number): string
483
+ - applyCurrencyOperation (function): function applyCurrencyOperation(currency: CurrencyDefinition, current: number, operation: CurrencyOperation, amount: number): CurrencyAdjustment
484
+ - resolveCurrencyDelta (function): function resolveCurrencyDelta(current: number, delta: number): { operation: CurrencyOperation; amount: number } — Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
485
+ - CurrencyDefinition (interface): interface CurrencyDefinition<TCurrencyId extends string = string>
486
+ - CurrencyOperation (type): type CurrencyOperation = "add" | "deduct"
487
+ - CurrencyAdjustment (type): type CurrencyAdjustment = | { success: true; newBalance: number; appliedDelta: number } | { success: false; reason: string }
488
+
489
+ ### @jgengine/core/economy/sharedWallet
490
+
491
+ - scopeKey (function): function scopeKey(scope: WalletScope): string
492
+ - userScope (function): function userScope(userId: string): WalletScope
493
+ - groupScope (function): function groupScope(groupId: string): WalletScope
494
+ - createWalletBook (function): function createWalletBook(): WalletBook
495
+ - balanceIn (function): function balanceIn(book: WalletBook, scope: WalletScope, currency: string): number
496
+ - grantTo (function): function grantTo(book: WalletBook, scope: WalletScope, currency: string, amount: number, by?: string): WalletBook
497
+ - chargeFrom (function): function chargeFrom(book: WalletBook, scope: WalletScope, currency: string, amount: number, by?: string): BookChargeResult
498
+ - contributionOf (function): function contributionOf(book: WalletBook, groupId: string, userId: string): Readonly<Record<string, number>>
499
+ - contributorsOf (function): function contributorsOf(book: WalletBook, groupId: string): string[]
500
+ - WalletScope (type): type WalletScope = | { kind: "user"; userId: string } | { kind: "group"; groupId: string }
501
+ - WalletBook (interface): interface WalletBook
502
+ - BookChargeResult (type): type BookChargeResult = | { status: "ok"; book: WalletBook } | { status: "rejected"; reason: "insufficient-funds" }
503
+
504
+ ### @jgengine/core/economy/techTree
505
+
506
+ - techPrerequisitesMet (function): function techPrerequisitesMet(state: TechState, node: TechNodeDef): boolean
507
+ - missingPrerequisites (function): function missingPrerequisites(state: TechState, node: TechNodeDef): string[]
508
+ - canUnlockTech (function): function canUnlockTech(defs: readonly TechNodeDef[], state: TechState, id: string): TechCheck
509
+ - grantTech (function): function grantTech(state: TechState, node: TechNodeDef): string[]
510
+ - availableTech (function): function availableTech(defs: readonly TechNodeDef[], state: TechState): TechNodeDef[]
511
+ - unlockedRecipes (function): function unlockedRecipes(defs: readonly TechNodeDef[], state: TechState): string[]
512
+ - createTechTree (function): function createTechTree(defs: readonly TechNodeDef[] = []): TechTree
513
+ - TechNodeDef (interface): interface TechNodeDef extends UnlockDef
514
+ - TechState (type): type TechState = UnlockState
515
+ - TechRejection (type): type TechRejection = | { reason: "unknown-node" } | { reason: "already-unlocked" } | { reason: "missing-prerequisites"; missing: readonly string[] }
516
+ - TechCheck (type): type TechCheck = { ok: true } | ({ ok: false } & TechRejection)
517
+ - TechTree (interface): interface TechTree
518
+
519
+ ### @jgengine/core/economy/wallet
520
+
521
+ - createEmptyWallet (function): function createEmptyWallet(): WalletState
522
+ - balance (function): function balance(state: WalletState, currency: string): number
523
+ - grant (function): function grant(state: WalletState, currency: string, amount: number): WalletState
524
+ - charge (function): function charge(state: WalletState, currency: string, amount: number): ChargeResult
525
+ - canAfford (function): function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean
526
+ - chargeAll (function): function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>): ChargeResult
527
+ - WalletState (interface): interface WalletState
528
+ - ChargeResult (type): type ChargeResult = { status: "ok"; state: WalletState } | { status: "rejected"; reason: "insufficient-funds" }
529
+
530
+ ### @jgengine/core/game/chat
531
+
532
+ - whisperChannelId (function): function whisperChannelId(a: string, b: string): string
533
+ - createChatRateLimiter (function): function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter
534
+ - createChat (function): function createChat(deps: ChatDeps): Chat
535
+ - ChatChannelKind (type): type ChatChannelKind = "global" | "party" | "proximity"
536
+ - ChatRateLimit (interface): interface ChatRateLimit
537
+ - ChatChannelDef (interface): interface ChatChannelDef
538
+ - ChatMessage (interface): interface ChatMessage
539
+ - ChatRecipients (type): type ChatRecipients = readonly string[] | "all"
540
+ - ChatSendResult (type): type ChatSendResult = | { message: ChatMessage; recipients: ChatRecipients } | { reason: string }
541
+ - ChatSnapshot (interface): interface ChatSnapshot
542
+ - Chat (interface): interface Chat
543
+ - ChatDeps (interface): interface ChatDeps
544
+ - DEFAULT_CHAT_RATE_LIMIT (const): const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit
545
+ - DEFAULT_CHAT_HISTORY_LIMIT (const): const DEFAULT_CHAT_HISTORY_LIMIT: 100
546
+ - DEFAULT_CHAT_BODY_LENGTH (const): const DEFAULT_CHAT_BODY_LENGTH: 500
547
+ - DEFAULT_PROXIMITY_CHAT_RADIUS (const): const DEFAULT_PROXIMITY_CHAT_RADIUS: 20
548
+ - WHISPER_CHANNEL_PREFIX (const): const WHISPER_CHANNEL_PREFIX: "whisper:"
549
+ - ChatRateLimiter (interface): interface ChatRateLimiter
550
+
551
+ ### @jgengine/core/game/cosmetics
552
+
553
+ - createCosmetics (function): function createCosmetics(deps: CosmeticsDeps = {}): Cosmetics
554
+ - CosmeticLoadoutDef (interface): interface CosmeticLoadoutDef
555
+ - CosmeticsChangedEvent (interface): interface CosmeticsChangedEvent
556
+ - CosmeticsEvents (interface): interface CosmeticsEvents
557
+ - CosmeticsDeps (interface): interface CosmeticsDeps
558
+ - Cosmetics (interface): interface Cosmetics
559
+
560
+ ### @jgengine/core/game/defineGame
561
+
562
+ - defineGame (function): function defineGame<TAssetRef extends ModelAssetRef, TMultiplayer>(config: GameDefinitionConfig<TAssetRef, TMultiplayer>): GameDefinition<TAssetRef, TMultiplayer>
563
+ - PhysicsConfig (interface): interface PhysicsConfig
564
+ - InventoryDeclaration (interface): interface InventoryDeclaration
565
+ - GameServerConfig (type): type GameServerConfig = "persistent" | { mode: string; [key: string]: unknown }
566
+ - GameLoop (interface): interface GameLoop<TContext = unknown>
567
+ - GameDefinition (interface): interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown>
568
+ - GameDefinitionConfig (type): type GameDefinitionConfig<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> = Omit<GameDefinition<TAssetRef, TMultiplayer>, "scene" | "assets"> & { assets?: AssetCatalog<TAssetRef>; }
569
+
570
+ ### @jgengine/core/game/events
571
+
572
+ - createGameEvents (function): function createGameEvents<TMap extends GameEventMap = GameEventMap>(): GameEvents<TMap>
573
+ - DeathReason (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string }
574
+ - EntityDiedEvent (interface): interface EntityDiedEvent
575
+ - LootGrantedEvent (interface): interface LootGrantedEvent
576
+ - InventoryAddedEvent (interface): interface InventoryAddedEvent
577
+ - QuestAcceptedEvent (interface): interface QuestAcceptedEvent
578
+ - QuestUpdatedEvent (interface): interface QuestUpdatedEvent
579
+ - QuestCompletedEvent (interface): interface QuestCompletedEvent
580
+ - SocialFriendAddedEvent (interface): interface SocialFriendAddedEvent
581
+ - SocialPartyJoinedEvent (interface): interface SocialPartyJoinedEvent
582
+ - SocialPartyLeftEvent (interface): interface SocialPartyLeftEvent
583
+ - SocialWorldInvitedEvent (interface): interface SocialWorldInvitedEvent
584
+ - SocialWorldAcceptedEvent (interface): interface SocialWorldAcceptedEvent
585
+ - ChatMessageEvent (interface): interface ChatMessageEvent
586
+ - StatLevelUpEvent (interface): interface StatLevelUpEvent
587
+ - EntityFloatTextEvent (interface): interface EntityFloatTextEvent
588
+ - CombatTelegraphEvent (interface): interface CombatTelegraphEvent
589
+ - CombatHitReactionEvent (interface): interface CombatHitReactionEvent
590
+ - ProjectileSettledEvent (interface): interface ProjectileSettledEvent
591
+ - WorldItemDroppedEvent (interface): interface WorldItemDroppedEvent
592
+ - WorldItemPickedUpEvent (interface): interface WorldItemPickedUpEvent
593
+ - CosmeticsChangedEvent (interface): interface CosmeticsChangedEvent
594
+ - EmotePlayedEvent (interface): interface EmotePlayedEvent
595
+ - PossessionSwappedEvent (interface): interface PossessionSwappedEvent
596
+ - FormChangedEvent (interface): interface FormChangedEvent
597
+ - GameEventMap (interface): interface GameEventMap
598
+ - GameEventHandler (type): type GameEventHandler<TPayload> = (payload: TPayload) => void
599
+ - GameEvents (interface): interface GameEvents<TMap extends GameEventMap = GameEventMap>
600
+
601
+ ### @jgengine/core/game/feed
602
+
603
+ - appendFeedEntry (function): function appendFeedEntry<T>(buffer: readonly FeedEntry<T>[], entry: FeedEntry<T>, limit: number): FeedEntry<T>[]
604
+ - recentFeedEntries (function): function recentFeedEntries<T>(buffer: readonly FeedEntry<T>[], limit?: number): FeedEntry<T>[]
605
+ - createGameFeed (function): function createGameFeed(options?: GameFeedOptions): GameFeed
606
+ - FeedEntry (interface): interface FeedEntry<T = unknown>
607
+ - GameFeedOptions (interface): interface GameFeedOptions
608
+ - GameFeed (interface): interface GameFeed
609
+
610
+ ### @jgengine/core/game/leaderboard
611
+
612
+ - createLeaderboard (function): function createLeaderboard(sink?: { onIncrement?(row: LeaderboardRow): void }): Leaderboard
613
+ - LeaderboardScope (type): type LeaderboardScope = "global" | "server" | "profile"
614
+ - LeaderboardTrackDef (interface): interface LeaderboardTrackDef
615
+ - LeaderboardRow (interface): interface LeaderboardRow
616
+ - IncrementResult (type): type IncrementResult = { status: "ok"; value: number } | { status: "rejected"; reason: "not-tracked" }
617
+ - Leaderboard (interface): interface Leaderboard
618
+
619
+ ### @jgengine/core/game/levelSequence
620
+
621
+ - createLevelSequence (function): function createLevelSequence<TLevelConfig>(config: LevelSequenceConfig<TLevelConfig>): LevelSequence<TLevelConfig> — A pure, deterministic level campaign: an ordered list of levels, each with its own opaque config, played through a `start` → (`clear` → `advance`)* → `complete` happy path, with `fail`/`retry` handling per-level attempts. Mirrors the reducer style of `game/race.ts` and `ai/spawnDirector.ts` — no I/O, no timers, just state transitions driven by the caller.
622
+ - LevelDescriptor (interface): interface LevelDescriptor<TLevelConfig>
623
+ - LevelSequenceConfig (interface): interface LevelSequenceConfig<TLevelConfig>
624
+ - LevelSequenceStatus (type): type LevelSequenceStatus = "idle" | "playing" | "cleared" | "failed" | "complete"
625
+ - CurrentLevel (interface): interface CurrentLevel<TLevelConfig>
626
+ - LevelSequenceProgress (interface): interface LevelSequenceProgress
627
+ - LevelSequence (interface): interface LevelSequence<TLevelConfig>
628
+
629
+ ### @jgengine/core/game/loadout
630
+
631
+ - createLoadouts (function): function createLoadouts(deps: LoadoutDeps): Loadouts
632
+ - LoadoutItemEntry (interface): interface LoadoutItemEntry
633
+ - LoadoutDef (interface): interface LoadoutDef
634
+ - LoadoutInventoryTransaction (interface): interface LoadoutInventoryTransaction
635
+ - LoadoutDeps (interface): interface LoadoutDeps
636
+ - Loadouts (interface): interface Loadouts
637
+
638
+ ### @jgengine/core/game/lootFilter
639
+
640
+ - evaluateLootFilter (function): function evaluateLootFilter(rules: readonly LootFilterRule[], item: LootFilterItem): LootFilterOverride — First matching rule wins (PoE/Last Epoch block semantics) — later rules never override an earlier match. Returns overrides only; fields the rule doesn't set are left for the caller's baseline (rarity style) to fill in.
641
+ - lootFilter (function): function lootFilter(rules: readonly LootFilterRule[]): readonly LootFilterRule[] — Validating factory — rule ids must be unique so authoring mistakes fail loudly.
642
+ - LootFilterItem (interface): interface LootFilterItem
643
+ - LootFilterCondition (interface): interface LootFilterCondition
644
+ - LootFilterRule (interface): interface LootFilterRule
645
+ - LootFilterOverride (interface): interface LootFilterOverride
646
+
647
+ ### @jgengine/core/game/lootTable
648
+
649
+ - createLootRegistry (function): function createLootRegistry(): LootRegistry
650
+ - lootTable (function): function lootTable(def: LootTableDef): LootTableDef
651
+ - grantDrops (function): function grantDrops(drops: Drop[], appliers: { putItem: (itemId: string, count: number) => unknown; grantCurrency: (currencyId: string, amount: number) => unknown; }): void
652
+ - LootEntry (interface): interface LootEntry
653
+ - LootTableDef (interface): interface LootTableDef
654
+ - Drop (interface): interface Drop
655
+ - LootRegistry (interface): interface LootRegistry
656
+
657
+ ### @jgengine/core/game/ping
658
+
659
+ - classifyPing (function): function classifyPing(hit: PointerHit, deps: PingClassifyDeps = {}, options: PingClassifyOptions = {}): PingCategory — Classify what a pointer/aim ray hit into a ping category. Entity hits resolve by catalog role (hostile → enemy, else location); object hits by an optional catalog category tag; open ground is a location ping.
660
+ - createPingSystem (function): function createPingSystem(deps: PingSystemDeps): PingSystem
661
+ - PingCategory (type): type PingCategory = string
662
+ - PingCategoryDef (interface): interface PingCategoryDef
663
+ - DEFAULT_PING_CATEGORIES (const): const DEFAULT_PING_CATEGORIES: Record<PingCategory, PingCategoryDef> — Content-agnostic default ping wheel: enemy / loot / location / danger.
664
+ - PING_FEED_ACTION (const): const PING_FEED_ACTION: "party.ping"
665
+ - PingClassifyDeps (interface): interface PingClassifyDeps
666
+ - PingClassifyOptions (interface): interface PingClassifyOptions
667
+ - PingPayload (interface): interface PingPayload
668
+ - PingFeedSink (interface): interface PingFeedSink
669
+ - PingParty (interface): interface PingParty
670
+ - PingSystemDeps (interface): interface PingSystemDeps
671
+ - PingSystem (interface): interface PingSystem
672
+
673
+ ### @jgengine/core/game/playableGame
674
+
675
+ - worldHealthBarAllowsRole (function): function worldHealthBarAllowsRole(roles: readonly CatalogEntityRole[] | undefined, role: CatalogEntityRole | undefined): boolean
676
+ - PointerConfig (interface): interface PointerConfig
677
+ - CameraFollowState (interface): interface CameraFollowState
678
+ - FirstPersonCameraConfig (interface): interface FirstPersonCameraConfig
679
+ - CameraRigKind (type): type CameraRigKind = | "orbit" | "first" | "topDown" | "rts" | "shoulder" | "lockOn" | "chase" | "observer" | "turntable" | "sideScroll" | "inspection" | "none" — Which camera rig the shell mounts. Every rig accepts `followEntityId: null` (avatar-less games — city-builders, card games, auto-battlers — still get a camera). Rigs are tuned through their config block below, never by writing camera positions from `onTick`. - `orbit` — third-person chase (the historical default; `perspective: "third"`). - `first` — pointer-lock mouse-look (`perspective: "first"`). - `topDown` — fixed height/pitch/yaw with decoupled follow (ARPG iso, top-down). - `rts` — free-pan / edge-scroll / rotate / zoom, optional follow. - `shoulder` — over-the-shoulder with ADS transition + shoulder swap. - `lockOn` — yaw bound to the player→target vector; move axis becomes strafe. - `chase` — speed-reactive vehicle chase (speed→FOV, spring arm, shake) + cockpit/hood/rear views. - `observer` — detached spectator/photo cam bound to any entity or fixed point; never reads player input. - `turntable` — slow auto-orbit of a fixed point: a rotating display stand for a scene. The friendly, flat spelling of `observer`'s point-orbit mode; providing `camera.turntable` selects it without an explicit `rig`. - `sideScroll` — fixed lateral follow (2.5D platformer/beat-'em-up side view); reads no player input. - `inspection` — model-viewer rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor; orbits a fixed point, reads no player/entity input. - `none` — no camera rig is mounted; use for HUD-only presentations or a game that manages its own camera.
680
+ - SideScrollCameraConfig (interface): interface SideScrollCameraConfig — Fixed lateral 2.5D follow (side-on platformer cam): the camera sits perpendicular to the travel axis, tracks the followed entity, and never reads player look input.
681
+ - TopDownCameraConfig (interface): interface TopDownCameraConfig — Fixed top-down / isometric rig (#23) — height/pitch/yaw + decoupled follow.
682
+ - RtsCameraConfig (interface): interface RtsCameraConfig extends TopDownCameraConfig — Free-pan / edge-scroll RTS rig (#24) — pan/rotate/zoom independent of any avatar.
683
+ - ShoulderCameraConfig (interface): interface ShoulderCameraConfig — Over-the-shoulder combat rig (#25) — offset, ADS, shoulder swap, decoupled reticle.
684
+ - LockOnCameraConfig (interface): interface LockOnCameraConfig — Lock-on / strafe rig (#26) — yaw bound to player→target, move axis becomes strafe.
685
+ - ChaseView (type): type ChaseView = "chase" | "cockpit" | "hood" | "rear" — Fixed interior view for the chase rig (#27).
686
+ - ChaseCameraConfig (interface): interface ChaseCameraConfig — Speed-reactive vehicle chase rig (#27) — speed→FOV, spring arm, procedural shake, interior views.
687
+ - ObserverCameraConfig (interface): interface ObserverCameraConfig — Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input.
688
+ - TurntableCameraConfig (interface): interface TurntableCameraConfig — Turntable / showcase rig — slowly auto-orbits a fixed world point (no player input), the way a museum turntable rotates an object on display. A flat, self-describing spelling of `observer`'s point-orbit mode: `target` names the point directly instead of `bind: { kind: "point", position }`. Set `camera.turntable` and the rig is inferred — you don't also write `rig`.
689
+ - InspectionZoomAnchor (type): type InspectionZoomAnchor = "target" | "cursor" | "center" — How scroll-zoom re-anchors the view for the inspection rig (#207.7): - `target` — dolly toward the orbit target (classic OrbitControls behavior). - `cursor` — dolly toward the point under the pointer. - `center` — dolly toward the viewport center; equivalent to `target` for an OrbitControls-driven rig, since the camera always faces `target` and that point already projects to the exact center of the viewport.
690
+ - InspectionCameraConfig (interface): interface InspectionCameraConfig — Model-viewer / inspection rig (#207.7) — orbit + pan + anchored zoom around a fixed point, never reads player input.
691
+ - CameraKeyframe (interface): interface CameraKeyframe — One stop on a scripted camera path (#29).
692
+ - CinematicCameraConfig (interface): interface CinematicCameraConfig — Scripted keyframe / path player (#29). When set it overrides the active rig.
693
+ - CameraShakeConfig (interface): interface CameraShakeConfig — Camera-shake / trauma defaults (#28). Any rig reads the trauma channel; game systems feed it via `cameraShake()`.
694
+ - CAMERA_FRUSTUM_DEFAULTS (const): const CAMERA_FRUSTUM_DEFAULTS: { readonly fov: 55; readonly near: 0.1; readonly far: 300; }
695
+ - GameCameraConfig (interface): interface GameCameraConfig
696
+ - EntitySpriteConfig (interface): interface EntitySpriteConfig
697
+ - WorldItemRenderConfig (interface): interface WorldItemRenderConfig
698
+ - VoxelCollisionConfig (interface): interface VoxelCollisionConfig — Player-vs-world collision for the first-person controller. Without this the shell keeps the player on flat ground at y=0. With `voxel: true` the shell resolves the player as a box against placed scene objects (each treated as a solid unit cell), so they stand on blocks, fall into holes, and are stopped by walls — the controller a block-building/mining game needs.
699
+ - ModelAnimationConfig (interface): interface ModelAnimationConfig — Rig playback for a `ModelConfig`'s GLTF animation clips — looping idles, one-shots, and held poses.
700
+ - ModelMaterialOverride (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every `MeshStandardMaterial` in the model's cloned scene graph.
701
+ - ModelConfig (interface): interface ModelConfig
702
+ - ObjectStyle (interface): interface ObjectStyle
703
+ - AmbientLightingConfig (interface): interface AmbientLightingConfig
704
+ - DirectionalLightingConfig (interface): interface DirectionalLightingConfig
705
+ - HemisphereLightingConfig (interface): interface HemisphereLightingConfig
706
+ - LightingConfig (interface): interface LightingConfig — Declarative lighting replacing the shell's hardcoded ambient/directional default (#207.5); mounts regardless of world kind, only when supplied.
707
+ - BackdropFogConfig (interface): interface BackdropFogConfig
708
+ - BackdropConfig (interface): interface BackdropConfig — Generic sky/background/fog for ANY world kind, including a custom `environment` component (#207.6).
709
+ - PlayerMovementConfig (interface): interface PlayerMovementConfig — Movement-control levers for the shell-driven local player walk controller.
710
+ - MovementCommitFrame (interface): interface MovementCommitFrame — One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`.
711
+ - PlayableGame (interface): interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEntity = never, TRenderObject = never>
712
+
713
+ ### @jgengine/core/game/progression
714
+
715
+ - evalCurve (function): function evalCurve(spec: Curve, x: number): number
716
+ - curve (function): function curve(spec: Curve): (x: number) => number
717
+ - leveling (function): function leveling(config: LevelingConfig): LevelingTrack
718
+ - CurveDef (type): type CurveDef = | { kind: "const"; value: number } | { kind: "linear"; base: number; per: number } | { kind: "power"; base: number; exponent: number } | { kind: "geometric"; base: number; ratio: number } | { kind: "steps"; values: number[] } | { kind: "piecewise"; points: [number, number][] }
719
+ - CurveShape (interface): interface CurveShape
720
+ - Curve (type): type Curve = CurveDef & CurveShape
721
+ - LevelProgress (interface): interface LevelProgress
722
+ - LevelingConfig (interface): interface LevelingConfig
723
+ - LevelingStatAccess (interface): interface LevelingStatAccess
724
+ - LevelingTrack (interface): interface LevelingTrack
725
+
726
+ ### @jgengine/core/game/quest
727
+
728
+ - createQuestJournal (function): function createQuestJournal(deps: QuestJournalDeps): QuestJournal
729
+ - applyQuestRewards (function): function applyQuestRewards(rewards: QuestRewards, appliers: { grantXp?(amount: number): void; grantEconomy?(currencyId: string, amount: number): void; grantItem?(inventoryId: string, itemId: string, count: number): void; grantUnlock?(unlockId: string): void; }): void
730
+ - createQuestEvaluator (function): function createQuestEvaluator(defs: QuestDef[] | Record<string, QuestDef>): QuestEvaluator
731
+ - QuestObjective (interface): interface QuestObjective
732
+ - QuestRewards (interface): interface QuestRewards
733
+ - QuestDef (interface): interface QuestDef
734
+ - QuestStatus (type): type QuestStatus = "active" | "completed"
735
+ - QuestObjectiveProgress (interface): interface QuestObjectiveProgress
736
+ - QuestInstance (interface): interface QuestInstance
737
+ - QuestSnapshotEntry (type): type QuestSnapshotEntry = { questId: string; status: QuestStatus; progress: Record<string, number>; }
738
+ - QuestJournalDeps (interface): interface QuestJournalDeps
739
+ - QuestJournal (interface): interface QuestJournal
740
+ - QuestAcceptOptions (interface): interface QuestAcceptOptions
741
+ - QuestTurnIn (interface): interface QuestTurnIn
742
+ - QuestEvaluator (interface): interface QuestEvaluator
743
+
744
+ ### @jgengine/core/game/race
745
+
746
+ - raceTrack (function): function raceTrack(config: RaceTrackConfig): RaceTrack — A race track is an ordered ring of checkpoint trigger volumes plus a lap count. The final checkpoint is the lap/finish line: a racer completes a lap by passing all checkpoints in order and hitting the last one.
747
+ - firstPastPost (function): function firstPastPost(count = 1): RaceWinCondition — Race ends when `count` racers have crossed the finish; ranking is the current standings order.
748
+ - topK (function): function topK(k: number): RaceWinCondition — Round-cut (Fall Guys): the first `k` finishers qualify; race resolves the moment `k` cross.
749
+ - everyoneFinishes (function): function everyoneFinishes(): RaceWinCondition — Every non-eliminated racer must finish.
750
+ - lastStanding (function): function lastStanding(): RaceWinCondition — Destruction-derby last-man-standing: ends when at most one racer is left un-eliminated.
751
+ - createRaceState (function): function createRaceState(config: RaceStateConfig): RaceState
752
+ - Checkpoint (interface): interface Checkpoint
753
+ - RaceTrackConfig (interface): interface RaceTrackConfig
754
+ - RaceTrack (interface): interface RaceTrack
755
+ - RacerProgress (interface): interface RacerProgress
756
+ - RaceEvent (type): type RaceEvent = | { type: "checkpoint.hit"; racerId: string; checkpoint: number; lap: number; time: number } | { type: "lap.completed"; racerId: string; lap: number; time: number } | { type: "position.changed"; racerId: string; position: number; previous: number } | { type: "race.finished"; ranking…
757
+ - RaceWinCondition (type): type RaceWinCondition = (standings: readonly RacerProgress[], track: RaceTrack) => readonly string[] | null
758
+ - RaceStateConfig (interface): interface RaceStateConfig
759
+ - RaceState (class): class RaceState — Race state machine (issue #87). Drive it each tick with `update(now, positions)` — `now` is game time (`ctx.time`), `positions` maps each racer to a world point tested against the ordered checkpoint volumes. It emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps cumulative split times for PB deltas, resolves a pluggable win condition (first-past-post, round-cut, derby last-standing), and `resetToCheckpoint` hands back a respawn pose at the racer's last checkpoint. `removeRacer` drops a racer mid-race and `reset` returns the whole instance to its pre-race state for reuse.
760
+
761
+ ### @jgengine/core/game/runDraft
762
+
763
+ - createRunModifierStack (function): function createRunModifierStack<TStat extends string = string, TData = unknown>(offers: readonly RunModifierOffer<TStat, TData>[]): RunModifierStack<TStat, TData>
764
+ - createRunDraft (function): function createRunDraft<TStat extends string = string, TData = unknown>(config: RunDraftConfig<TStat, TData>): RunDraft<TStat, TData>
765
+ - RunModifierOffer (interface): interface RunModifierOffer<TStat extends string = string, TData = unknown>
766
+ - RunModifierPick (interface): interface RunModifierPick
767
+ - RunModifierStack (interface): interface RunModifierStack<TStat extends string = string, TData = unknown>
768
+ - RunDraftConfig (interface): interface RunDraftConfig<TStat extends string = string, TData = unknown>
769
+ - RunDraft (interface): interface RunDraft<TStat extends string = string, TData = unknown>
770
+
771
+ ### @jgengine/core/game/social
772
+
773
+ - createSocial (function): function createSocial(deps: SocialDeps): Social
774
+ - PresenceInfo (interface): interface PresenceInfo
775
+ - EmoteEntityLookup (interface): interface EmoteEntityLookup
776
+ - EmoteSpatialLookup (interface): interface EmoteSpatialLookup
777
+ - EmotesDeps (interface): interface EmotesDeps
778
+ - SocialDeps (interface): interface SocialDeps
779
+ - FriendEntry (interface): interface FriendEntry
780
+ - FriendsSnapshot (interface): interface FriendsSnapshot
781
+ - FriendRequestEntry (interface): interface FriendRequestEntry
782
+ - Friends (interface): interface Friends
783
+ - PartyRole (type): type PartyRole = "leader" | "member"
784
+ - PartyMemberEntry (interface): interface PartyMemberEntry
785
+ - PartyConfig (interface): interface PartyConfig
786
+ - PartyInviteEntry (interface): interface PartyInviteEntry
787
+ - Party (interface): interface Party
788
+ - DEFAULT_EMOTE_RADIUS (const): const DEFAULT_EMOTE_RADIUS: 20
789
+ - EmoteBroadcastResult (interface): interface EmoteBroadcastResult
790
+ - Emotes (interface): interface Emotes
791
+ - WorldInviteTarget (interface): interface WorldInviteTarget
792
+ - WorldInvite (interface): interface WorldInvite extends WorldInviteTarget
793
+ - WorldInvites (interface): interface WorldInvites
794
+ - Social (interface): interface Social
795
+
796
+ ### @jgengine/core/game/spawnPoints
797
+
798
+ - createSpawnPoints (function): function createSpawnPoints(): SpawnPoints
799
+ - SpawnPointPose (interface): interface SpawnPointPose
800
+ - RespawnTarget (interface): interface RespawnTarget
801
+ - SpawnPoints (interface): interface SpawnPoints
802
+
803
+ ### @jgengine/core/game/trade
804
+
805
+ - canAffordCosts (function): function canAffordCosts(balances: Record<string, number>, costs: Record<string, number>): string | null
806
+ - resolveBuy (function): function resolveBuy(itemId: string, trade: TradeField | null | undefined, shopId: string, count: number, balances: Record<string, number>): TradeResolution
807
+ - resolveSell (function): function resolveSell(itemId: string, trade: TradeField | null | undefined, count: number): TradeResolution
808
+ - applyTradeOutcome (function): function applyTradeOutcome(outcome: TradeOutcome, appliers: { adjustItem(itemId: string, count: number): { reason: string } | null; adjustCurrency(delta: Record<string, number>): void; }): { reason: string } | null
809
+ - createTradeSystem (function): function createTradeSystem(deps: TradeSystemDeps): TradeSystem
810
+ - TradeField (interface): interface TradeField
811
+ - TradeRejection (interface): interface TradeRejection
812
+ - TradeWallet (interface): interface TradeWallet
813
+ - TradeInventory (interface): interface TradeInventory
814
+ - TradeSystemDeps (interface): interface TradeSystemDeps
815
+ - TradeParty (interface): interface TradeParty
816
+ - TradeSystem (interface): interface TradeSystem
817
+ - TradeOutcome (interface): interface TradeOutcome
818
+ - TradeResolution (type): type TradeResolution = | { status: "ok"; outcome: TradeOutcome } | { status: "rejected"; reason: string }
819
+
820
+ ### @jgengine/core/game/unlocks
821
+
822
+ - hasUnlock (function): function hasUnlock(granted: UnlockState, unlockId: string): boolean
823
+ - grantUnlock (function): function grantUnlock(granted: UnlockState, unlockId: string): string[]
824
+ - unlockTree (function): function unlockTree(defs: readonly UnlockDef[], categoryId: string): UnlockDef[]
825
+ - createUnlockCatalog (function): function createUnlockCatalog(defs: readonly UnlockDef[] = []): UnlockCatalog
826
+ - createUnlocks (function): function createUnlocks(defs: UnlockDef[] = []): Unlocks
827
+ - UnlockDef (interface): interface UnlockDef
828
+ - UnlockState (type): type UnlockState = readonly string[]
829
+ - UnlockCatalog (interface): interface UnlockCatalog
830
+ - Unlocks (interface): interface Unlocks
831
+
832
+ ### @jgengine/core/game/worldItem
833
+
834
+ - scatterOffset (function): function scatterOffset(rng: () => number, options: ScatterOptions = DEFAULT_SCATTER): EntityPosition — Random offset within an annulus `[minRadius, radius]` around the origin — the on-death scatter impulse.
835
+ - scatterPosition (function): function scatterPosition(origin: EntityPosition, rng: () => number, options: ScatterOptions = DEFAULT_SCATTER): EntityPosition
836
+ - selectNearestWorldItem (function): function selectNearestWorldItem(candidates: readonly { instanceId: string; position: EntityPosition }[], from: EntityPosition, radius: number): string | null — Pure pickup-radius + nearest-item selection, usable without a live store (click-to-grab, proximity prompts).
837
+ - createWorldItemStore (function): function createWorldItemStore(deps: WorldItemStoreDeps): WorldItemStore
838
+ - resolveDeathDrops (function): function resolveDeathDrops(drops: readonly Drop[], options: ResolveDeathDropsOptions): ResolvedDeathDrops — Splits rolled death drops between direct grants and scattered ground items. `mode: "grant"` is the legacy behavior (loot straight to inventory); `mode: "world"` routes item drops through a scatter impulse and leaves currency drops granting directly (coins fly to the killer, gear hits the ground).
839
+ - resolveWorldItemPresentation (function): function resolveWorldItemPresentation(item: LootFilterItem, rarityStyle: Record<string, RarityStyle> | undefined, rules: readonly LootFilterRule[] | undefined): WorldItemPresentation — Composes the baseline rarity→beam/color/label render binding (#32, catalog data the game supplies) with the loot-filter rule overrides (#33). A matching rule wins field-by-field; unmatched fields fall back to the rarity's baseline style.
840
+ - WORLD_ITEM_ENTITY_NAME (const): const WORLD_ITEM_ENTITY_NAME: "world_item" — Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object).
841
+ - DEFAULT_RARITY (const): const DEFAULT_RARITY: "common"
842
+ - DEFAULT_PICKUP_RADIUS (const): const DEFAULT_PICKUP_RADIUS: 2
843
+ - ScatterOptions (interface): interface ScatterOptions
844
+ - DEFAULT_SCATTER (const): const DEFAULT_SCATTER: ScatterOptions
845
+ - WorldItemRecord (interface): interface WorldItemRecord
846
+ - WorldItemSpawnInput (interface): interface WorldItemSpawnInput
847
+ - WorldItemStoreDeps (interface): interface WorldItemStoreDeps
848
+ - WorldItemStore (interface): interface WorldItemStore
849
+ - ResolveDeathDropsOptions (interface): interface ResolveDeathDropsOptions
850
+ - ResolvedDeathDrops (interface): interface ResolvedDeathDrops
851
+ - RarityStyle (interface): interface RarityStyle
852
+ - WorldItemPresentation (interface): interface WorldItemPresentation
853
+
854
+ ### @jgengine/core/index
855
+
856
+ - parseSaveAutoMs (function): function parseSaveAutoMs(auto: string): number
857
+ - saveScopeIncludesPlayer (function): function saveScopeIncludesPlayer(scope: SaveScope): boolean
858
+ - saveScopeIncludesChunks (function): function saveScopeIncludesChunks(scope: SaveScope): boolean
859
+ - isSaveEnabled (function): function isSaveEnabled(config: SaveConfig): config is Exclude<SaveConfig, "none">
860
+ - SaveScope (type): type SaveScope = "player" | "chunks" | "player+chunks"
861
+ - SaveConfig (type): type SaveConfig = | "none" | { auto: string; scope: SaveScope; }
862
+ - createEmptyServerRow (function): function createEmptyServerRow(): RuntimeServerRow
863
+ - createEmptyPlayerRow (function): function createEmptyPlayerRow(userId: string): RuntimePlayerRow
864
+ - createRuntimeSnapshot (function): function createRuntimeSnapshot(args: { gameId: string; serverId: string; server?: RuntimeServerRow; players?: Record<string, RuntimePlayerRow>; chunks?: Record<string, RuntimeChunkRow>; }): GameRuntimeSnapshot
865
+ - markServerDirty (function): function markServerDirty(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot
866
+ - markPlayerDirty (function): function markPlayerDirty(snapshot: GameRuntimeSnapshot, userId: string): GameRuntimeSnapshot
867
+ - clearDirtyFlags (function): function clearDirtyFlags(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot
868
+ - splitProfilePlayer (function): function splitProfilePlayer(player: RuntimePlayerRow): { persistent: RuntimePlayerRow; session: Record<string, unknown>; }
869
+ - RUNTIME_SNAPSHOT_VERSION (const): const RUNTIME_SNAPSHOT_VERSION: 1
870
+ - RuntimeEntityRow (type): type RuntimeEntityRow = { instanceId: string; catalogId: string; position?: [number, number, number]; rotationY?: number; parentSpace?: string; group?: string; stats?: Record<string, { current: number; max: number; min?: number }>; targetInstanceId?: string | null; userId?: string; }
871
+ - RuntimeObjectRow (type): type RuntimeObjectRow = { instanceId: string; catalogId: string; position: [number, number, number]; rotationY?: number; parentSpace?: string; flags?: Record<string, unknown>; }
872
+ - RuntimeInventorySlot (type): type RuntimeInventorySlot = { item: string; count: number; slot?: number; }
873
+ - RuntimePlayerRow (type): type RuntimePlayerRow = { userId: string; inventories: Record<string, RuntimeInventorySlot[]>; economy: Record<string, number>; unlocks: string[]; quests?: unknown; social?: unknown; leaderboard?: Record<string, number>; session?: Record<string, unknown>; }
874
+ - RuntimeChunkRow (type): type RuntimeChunkRow = { chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string, unknown>; }
875
+ - RuntimeServerRow (type): type RuntimeServerRow = { entities: RuntimeEntityRow[]; objects: RuntimeObjectRow[]; session: Record<string, unknown>; feeds?: Record<string, unknown[]>; }
876
+ - RuntimeProfileRow (type): type RuntimeProfileRow = { userId: string; gameId: string; player: RuntimePlayerRow; updatedAt: number; }
877
+ - GameRuntimeSnapshot (type): type GameRuntimeSnapshot = { version: number; gameId: string; serverId: string; server: RuntimeServerRow; players: Record<string, RuntimePlayerRow>; chunks: Record<string, RuntimeChunkRow>; revision: number; dirty: { server: boolean; players: string[]; chunks: string[]; }; }
878
+ - runCommand (function): function runCommand<TInput>(snapshot: import("./snapshot").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): RunCommandResult
879
+ - CommandValidationError (type): type CommandValidationError = { reason: string }
880
+ - CommandDef (type): type CommandDef<TInput = unknown> = { validate: (snapshot: import("./snapshot").GameRuntimeSnapshot, input: TInput) => CommandValidationError | null; apply: ( snapshot: import("./snapshot").GameRuntimeSnapshot, input: TInput, ) => import("./snapshot").GameRuntimeSnapshot; }
881
+ - RunCommandResult (type): type RunCommandResult = | { ok: true; snapshot: import("./snapshot").GameRuntimeSnapshot } | { ok: false; reason: string }
882
+ - createGameRuntime (function): function createGameRuntime(definition: GameRuntimeDefinition): GameRuntime
883
+ - ServerLoopHooks (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeLoopContext, dtSeconds: number) => void; }
884
+ - RuntimeInitContext (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; }
885
+ - RuntimeLoopContext (type): type RuntimeLoopContext = RuntimeInitContext & { player: { userId: string; isNew: boolean; }; }
886
+ - GameRuntimeDefinition (type): type GameRuntimeDefinition = { gameId: string; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; }
887
+ - HydrateInput (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; }
888
+ - GameRuntime (type): type GameRuntime = { gameId: string; save: SaveConfig; hydrate: (input: HydrateInput) => GameRuntimeSnapshot; runCommand: ( snapshot: GameRuntimeSnapshot, actorUserId: string, commandName: string, input: unknown, ) => ReturnType<typeof runCommand>; tick: (snapshot: GameRuntimeSnapshot, dtSeconds: nu…
889
+ - convex (function): function convex(config?: { topology?: MultiplayerTopology }): MultiplayerAdapterConfig
890
+ - ws (function): function ws(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig
891
+ - fly (function): function fly(config: { app: string; topology?: MultiplayerTopology; path?: string }): MultiplayerAdapterConfig
892
+ - socketIo (function): function socketIo(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig
893
+ - p2p (function): function p2p(config?: { topology?: MultiplayerTopology; room?: string }): MultiplayerAdapterConfig
894
+ - lan (function): function lan(config?: { topology?: MultiplayerTopology; port?: number; path?: string; }): MultiplayerAdapterConfig
895
+ - offline (function): function offline(): MultiplayerAdapterConfig
896
+ - adapterOf (function): function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null
897
+ - multiplayerAdapterKind (function): function multiplayerAdapterKind(multiplayer: unknown): string | null
898
+ - servers (function): function servers(config: ServersPoolConfig): import("/home/runner/work/jgengine/jgengine/packages/core/src/runtime/adapter").ServersPoolConfig
899
+ - MultiplayerTopology (type): type MultiplayerTopology = "shared" | "lobbies" | "private"
900
+ - MultiplayerAdapterConfig (type): type MultiplayerAdapterConfig = | { kind: "convex"; topology?: MultiplayerTopology } | { kind: "ws"; topology?: MultiplayerTopology; url?: string } | { kind: "socketio"; topology?: MultiplayerTopology; url?: string } | { kind: "p2p"; topology?: MultiplayerTopology; room?: string } | { kind: "lan"; t…
901
+ - ServersPoolConfig (type): type ServersPoolConfig = { maxServers: number; slotsPerServer: number; minPlayersToStart?: number; adapter: MultiplayerAdapterConfig; }
902
+ - JoinServerResult (type): type JoinServerResult = { serverId: string; isNew: boolean; }
903
+ - RunCommandArgs (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; }
904
+ - TransportRunCommandResult (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string }
905
+ - GameRuntimeServerView (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown; updatedAt: number; }
906
+ - GameRuntimePlayerView (type): type GameRuntimePlayerView = { userId: string; gameId: string; playerState: unknown; updatedAt: number; }
907
+ - GameRuntimeFeedView (type): type GameRuntimeFeedView = { action: string; entries: unknown[]; }
908
+ - GameRuntimeTransport (type): type GameRuntimeTransport = { joinServer: (args: { gameId: string; serverId?: string }) => Promise<JoinServerResult>; leaveServer: (args: { serverId: string }) => Promise<void>; runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>; }
909
+ - FeedUnsubscribe (type): type FeedUnsubscribe = () => void
910
+ - GameRuntimeFeeds (type): type GameRuntimeFeeds = { subscribeServer: ( serverId: string, onChange: (view: GameRuntimeServerView | null) => void, ) => FeedUnsubscribe; subscribePlayer: ( args: { serverId: string }, onChange: (view: GameRuntimePlayerView | null) => void, ) => FeedUnsubscribe; subscribeFeed: ( args: { serverId:…
911
+ - GameBackend (type): type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = { transport: GameRuntimeTransport; feeds?: GameRuntimeFeeds; presence?: PresenceTransport<TPresenceRow, TPresenceLocation, TGameId>; }
912
+ - PresencePoseRow (type): type PresencePoseRow = { userId: string; position: { x: number; y: number; z: number }; rotationY: number; rotationPitch: number; lastSeenAt: number; }
913
+ - PresenceSync (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; }
914
+ - LiveGameBackend (type): type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & { presenceSync: PresenceSync; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; chatSyncFor…
915
+ - MultiplayerSession (type): type MultiplayerSession = { gameId: string; userId: string; backend: LiveGameBackend; feedActions: string[]; }
916
+
917
+ ### @jgengine/core/input/actionBindings
918
+
919
+ - bindingMatches (function): function bindingMatches<TCode extends string>(code: TCode, binding: ActionBinding<TCode>): boolean
920
+ - resolveBoundAction (function): function resolveBoundAction<TAction extends string, TCode extends string>(code: TCode, bindings: ActionBindingMap<TAction, TCode>): TAction | null — First action (in the map's key order) bound to the given control code, or null.
921
+ - normalizeKeyCode (function): function normalizeKeyCode(code: string): string — Collapse left/right modifier variants of a KeyboardEvent.code so bindings store one logical key.
922
+ - actionRepeatMs (function): function actionRepeatMs(codes: ActionCodes | undefined): number | undefined
923
+ - shouldDispatchAction (function): function shouldDispatchAction({ pressed, down, repeatMs, lastFiredAt, now }: ShouldDispatchActionInput): boolean
924
+ - toActionStateBindingMap (function): function toActionStateBindingMap<TAction extends string, TCode extends string>(map: ActionCodesMap<TAction, TCode>): ActionStateBindingMap<TAction, TCode>
925
+ - hotbarSlotBindings (function): function hotbarSlotBindings(count: number, options?: { action?: (slot: number) => string }): ActionCodesMap
926
+ - hotbarSlotActionIndex (function): function hotbarSlotActionIndex(action: string): number | null — 0-based slot index for a hotbar-slot action name (`slot1`, `hotbarSlot1`), or null.
927
+ - isHotbarSlotAction (function): function isHotbarSlotAction(action: string): boolean
928
+ - resolveActionCommand (function): function resolveActionCommand(action: string, has: (command: string) => boolean, reserved: ReadonlySet<string>): string | null — Convention mapping a bound input action to the command it fires: the action's own name when a command by that name exists, otherwise a `ui.<action>` fallback. Reserved actions (consumed natively by the shell) and hotbar slots yield null so the shell can handle them itself.
929
+ - bindingLabel (function): function bindingLabel(code: string): string
930
+ - actionLabel (function): function actionLabel(map: ActionCodesMap, action: string): string | null — Short display label for an action's first bound code, or null when unbound.
931
+ - createActionStateTracker (function): function createActionStateTracker<TAction extends string, TCode extends string = string>(map: ActionStateBindingMap<TAction, TCode>): ActionStateTracker<TAction>
932
+ - ActionBinding (interface): interface ActionBinding<TCode extends string = string> — Action-binding model: games bind semantic actions ("jump", "interact") to physical control codes; capture layers resolve raw events through this map so gameplay code never sees keycodes. Control codes are plain strings, so the same model serves keyboard codes, mouse buttons, touch controls, or gamepad inputs.
933
+ - ActionBindingMap (type): type ActionBindingMap<TAction extends string, TCode extends string = string> = Record< TAction, ActionBinding<TCode> >
934
+ - ActionBindingModes (interface): interface ActionBindingModes<TCode extends string = string>
935
+ - ActionBindingConfig (type): type ActionBindingConfig<TCode extends string = string> = ActionBinding<TCode>[] | ActionBindingModes<TCode>
936
+ - ActionStateBindingMap (type): type ActionStateBindingMap<TAction extends string, TCode extends string = string> = Record< TAction, ActionBindingConfig<TCode> >
937
+ - ActionCodes (type): type ActionCodes<TCode extends string = string> = | readonly TCode[] | { hold?: readonly TCode[]; toggle?: readonly TCode[]; repeatMs?: number }
938
+ - ActionCodesMap (type): type ActionCodesMap<TAction extends string = string, TCode extends string = string> = Record< TAction, ActionCodes<TCode> >
939
+ - ShouldDispatchActionInput (interface): interface ShouldDispatchActionInput
940
+ - ActionStateTracker (interface): interface ActionStateTracker<TAction extends string>
941
+
942
+ ### @jgengine/core/input/axisInput
943
+
944
+ - clampAxis (function): function clampAxis(value: number, range: AxisRange): number
945
+ - rampToward (function): function rampToward(current: number, target: number, rate: number, dt: number): number
946
+ - AxisInput (interface): interface AxisInput
947
+ - NEUTRAL_AXIS (const): const NEUTRAL_AXIS: AxisInput
948
+ - AxisName (type): type AxisName = keyof AxisInput
949
+ - AxisBinding (interface): interface AxisBinding
950
+ - AxisBindingMap (type): type AxisBindingMap = Record<AxisName, AxisBinding>
951
+ - AxisChannelConfig (interface): interface AxisChannelConfig
952
+ - AxisRange (interface): interface AxisRange
953
+ - AXIS_RANGE (const): const AXIS_RANGE: Record<AxisName, AxisRange>
954
+ - AxisChannel (class): class AxisChannel — Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from a gamepad axis via `setAnalog`. `sample(dt, isDown)` folds the current held-key state into the smoothed value; a `setAnalog` override replaces the digital target for that axis until cleared.
955
+ - DRIVE_AXIS_BINDINGS (const): const DRIVE_AXIS_BINDINGS: AxisBindingMap
956
+
957
+ ### @jgengine/core/input/gestureSurface
958
+
959
+ - createGestureSurfaceTracker (function): function createGestureSurfaceTracker(bindings: TouchGestureBindings, tuning: GestureSurfaceTuning = DEFAULT_GESTURE_TUNING): GestureSurfaceTracker
960
+ - GestureSurfaceTuning (interface): interface GestureSurfaceTuning
961
+ - DEFAULT_GESTURE_TUNING (const): const DEFAULT_GESTURE_TUNING: GestureSurfaceTuning
962
+ - GestureSurfaceTracker (interface): interface GestureSurfaceTracker
963
+
964
+ ### @jgengine/core/input/lookChannel
965
+
966
+ - createLookChannel (function): function createLookChannel({ sensitivity, maxVerticalOffset = Infinity }: LookChannelOptions): LookChannel
967
+ - LookDeltas (interface): interface LookDeltas — Per-frame look channel shared between an event-driven capture layer (writer of raw pointer deltas) and a frame-driven controller (consumer), plus the latest committed pose for same-frame readers such as presence sync. Kept as plain mutable state on purpose: routing per-frame deltas through a reactive store would notify subscribers every frame for state no UI reads.
968
+ - LookChannel (interface): interface LookChannel
969
+ - LookChannelOptions (interface): interface LookChannelOptions
970
+
971
+ ### @jgengine/core/input/pointer
972
+
973
+ - aimToPoint (function): function aimToPoint(origin: PointerVec3, point: PointerVec3): Aim — Build an `origin → point` aim for `item.use` / projectiles, firing toward the cursor.
974
+ - moveTargetFromHit (function): function moveTargetFromHit(hit: PointerHit): PointerVec3 — The move-to target of a pointer hit — sugar over `hit.point`.
975
+ - groundOf (function): function groundOf(hit: PointerHit): readonly [number, number] — Project a pointer hit onto the XZ plane for navmesh routing (`findPath` takes `[x, z]`).
976
+ - createDragCapture (function): function createDragCapture(config: DragCaptureConfig = {}): DragCapture
977
+ - PointerVec3 (type): type PointerVec3 = readonly [number, number, number]
978
+ - PointerHit (interface): interface PointerHit — Renderer-free result of a screen→world raycast. The shell's pointer service produces this from the cursor; core-side gameplay (item.use aim, click-to-move, ground-target abilities, pings) consumes it without touching three.js.
979
+ - PointerButton (type): type PointerButton = "primary" | "secondary" | "middle"
980
+ - DragState (interface): interface DragState — Snapshot of an in-progress or just-released drag: origin/current cursor points plus the clamped pull vector.
981
+ - DragResult (type): type DragResult = DragState
982
+ - DragCaptureConfig (interface): interface DragCaptureConfig
983
+ - DragCapture (interface): interface DragCapture — Renderer-agnostic drag-capture / pull-vector state machine for slingshot-style aiming, drawback abilities, and similar gestures.
984
+
985
+ ### @jgengine/core/input/touchGestures
986
+
987
+ - createTouchGestureTracker (function): function createTouchGestureTracker(tuning: TouchGestureTuning): TouchGestureTracker
988
+ - TouchGestureTuning (interface): interface TouchGestureTuning — Single-finger gesture disambiguation for touch look controls: one touch stream drives look-drag, tap, and long-press-hold without fighting. Movement past the tap threshold commits the gesture to "look" and cancels tap/hold; a still finger that lifts quickly is a tap; a still finger held past the long-press delay is a hold. Timestamps are injected so the state machine stays pure; the capture layer owns timers and touch identifiers.
989
+ - TouchGestureEnd (type): type TouchGestureEnd = "tap" | "hold-end" | null
990
+ - TouchGestureTracker (interface): interface TouchGestureTracker
991
+
992
+ ### @jgengine/core/input/touchScheme
993
+
994
+ - touchCode (function): function touchCode(action: string): string
995
+ - withTouchCodes (function): function withTouchCodes(map: ActionCodesMap | undefined): ActionCodesMap — Every action gains a synthetic touch code alongside its physical codes.
996
+ - touchActionLabel (function): function touchActionLabel(action: string): string
997
+ - deriveTouchScheme (function): function deriveTouchScheme(input: ActionCodesMap | undefined, { reserved, firstPerson, config }: DeriveTouchSchemeOptions): TouchScheme | null — Null means "render no touch controls" — either the game opted out or there is nothing to synthesize.
998
+ - TOUCH_CODE_PREFIX (const): const TOUCH_CODE_PREFIX: "touch:"
999
+ - TouchDragBinding (interface): interface TouchDragBinding
1000
+ - TouchGestureBindings (interface): interface TouchGestureBindings
1001
+ - TouchButtonSpec (interface): interface TouchButtonSpec
1002
+ - TouchControlsConfig (interface): interface TouchControlsConfig
1003
+ - TouchJoystick (interface): interface TouchJoystick
1004
+ - TouchButton (interface): interface TouchButton
1005
+ - TouchScheme (interface): interface TouchScheme
1006
+ - DeriveTouchSchemeOptions (interface): interface DeriveTouchSchemeOptions
1007
+
1008
+ ### @jgengine/core/interaction/contextMenu
1009
+
1010
+ - contextVerb (function): function contextVerb(label: string, command: string, args?: Record<string, unknown>): ContextVerb
1011
+ - buildContextMenu (function): function buildContextMenu(input: BuildContextMenuInput): ContextMenu | null — Assemble a menu from a target's catalog verbs; null when the target lists none.
1012
+ - contextVerbInput (function): function contextVerbInput(menu: ContextMenu, verb: ContextVerb): Record<string, unknown> — Command input a chosen verb dispatches: the verb's own args, plus the target id and the world point, so a single handler can walk the actor to the target then perform it.
1013
+ - ContextTargetKind (type): type ContextTargetKind = "entity" | "object"
1014
+ - ContextVerb (interface): interface ContextVerb — One right-click verb: a label plus the command it dispatches (walk-then-act supported by args).
1015
+ - ContextMenu (interface): interface ContextMenu
1016
+ - BuildContextMenuInput (interface): interface BuildContextMenuInput
1017
+
1018
+ ### @jgengine/core/interaction/proximityPrompt
1019
+
1020
+ - keybind (function): function keybind(actionId: string): KeybindPromptDisplay
1021
+ - gauge (function): function gauge(gaugeId: string): GaugePromptDisplay
1022
+ - label (function): function label(text: string): LabelPromptDisplay
1023
+ - command (function): function command(name: string, input?: unknown): PromptCommand
1024
+ - proximityPrompt (function): function proximityPrompt({ radius, display, invoke = null }: ProximityPromptConfig): ProximityPrompt
1025
+ - resolveActivePrompt (function): function resolveActivePrompt<T extends PositionedPrompt>(playerPosition: PromptPoint, prompts: readonly T[]): T | null — Nearest prompt strictly within its radius wins; a higher-priority prompt in range beats any lower-priority one regardless of distance; equal priority and distance keep the earliest prompt in the list.
1026
+ - promptDisplaysEqual (function): function promptDisplaysEqual(a: ProximityPromptDisplay, b: ProximityPromptDisplay): boolean
1027
+ - promptCommandsEqual (function): function promptCommandsEqual(a: PromptCommand | null, b: PromptCommand | null): boolean
1028
+ - positionedPromptsEqual (function): function positionedPromptsEqual(a: PositionedPrompt, b: PositionedPrompt): boolean
1029
+ - PromptPoint (interface): interface PromptPoint
1030
+ - KeybindPromptDisplay (interface): interface KeybindPromptDisplay
1031
+ - GaugePromptDisplay (interface): interface GaugePromptDisplay
1032
+ - LabelPromptDisplay (interface): interface LabelPromptDisplay
1033
+ - ProximityPromptDisplay (type): type ProximityPromptDisplay = KeybindPromptDisplay | GaugePromptDisplay | LabelPromptDisplay
1034
+ - PromptCommand (interface): interface PromptCommand
1035
+ - ProximityPrompt (interface): interface ProximityPrompt
1036
+ - ProximityPromptConfig (interface): interface ProximityPromptConfig
1037
+ - PositionedPrompt (interface): interface PositionedPrompt
1038
+
1039
+ ### @jgengine/core/interaction/qte
1040
+
1041
+ - evaluateQteSequence (function): function evaluateQteSequence(steps: readonly QteStep[], inputs: readonly QteInputEvent[]): QteOutcome
1042
+ - pendingQteStep (function): function pendingQteStep(steps: readonly QteStep[], elapsedSeconds: number): QteStep | null
1043
+ - qteProgress (function): function qteProgress(steps: readonly QteStep[], elapsedSeconds: number): number
1044
+ - QteStep (interface): interface QteStep
1045
+ - QteInputEvent (interface): interface QteInputEvent
1046
+ - QteOutcome (type): type QteOutcome = | { status: "success" } | { status: "fail"; atStep: string; reason: "missed-window" | "wrong-action" | "too-early" }
1047
+
1048
+ ### @jgengine/core/interaction/skillCheck
1049
+
1050
+ - skillCheckMarkerPosition (function): function skillCheckMarkerPosition(config: SkillCheckConfig, elapsedSeconds: number): number
1051
+ - skillCheckZoneAt (function): function skillCheckZoneAt(config: SkillCheckConfig, elapsedSeconds: number): SkillCheckZone
1052
+ - evaluateSkillCheck (function): function evaluateSkillCheck(config: SkillCheckConfig, elapsedSeconds: number): SkillCheckResult
1053
+ - SkillCheckZone (interface): interface SkillCheckZone
1054
+ - SkillCheckConfig (interface): interface SkillCheckConfig
1055
+ - SkillCheckResult (interface): interface SkillCheckResult
1056
+
1057
+ ### @jgengine/core/inventory/inventoryModel
1058
+
1059
+ - createEmptyInventory (function): function createEmptyInventory(layout: InventoryLayout): InventoryState
1060
+ - putItem (function): function putItem(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, itemId: string, count: number, options?: { slot?: number }): PutResult
1061
+ - takeItem (function): function takeItem(state: InventoryState, itemId: string, count: number): TakeResult
1062
+ - countItem (function): function countItem(state: InventoryState, itemId: string): number
1063
+ - hasItem (function): function hasItem(state: InventoryState, itemId: string, count: number): boolean
1064
+ - moveItem (function): function moveItem(from: InventoryState, fromSlot: number, to: InventoryState, toLayout: InventoryLayout, traits: ItemTraits, toSlot?: number): MoveResult
1065
+ - createInventorySet (function): function createInventorySet<TId extends string>(layouts: Record<TId, InventoryLayout>, traits: ItemTraits): InventorySet<TId>
1066
+ - ItemTraits (interface): interface ItemTraits
1067
+ - InventoryLayout (interface): interface InventoryLayout
1068
+ - InventorySlot (type): type InventorySlot = { itemId: string; count: number } | null
1069
+ - InventoryState (interface): interface InventoryState
1070
+ - PutResult (type): type PutResult = | { status: "ok"; state: InventoryState } | { status: "rejected"; reason: "no-space" | "wrong-kind" | "slot-occupied" | "invalid-slot" }
1071
+ - TakeResult (type): type TakeResult = { status: "ok"; state: InventoryState } | { status: "rejected"; reason: "insufficient" }
1072
+ - MoveResult (type): type MoveResult = | { status: "ok"; from: InventoryState; to: InventoryState } | { status: "rejected"; reason: "invalid-slot" | "empty-slot" | "wrong-kind" | "no-space" }
1073
+ - InventorySet (interface): interface InventorySet<TId extends string>
1074
+
1075
+ ### @jgengine/core/inventory/shapedGrid
1076
+
1077
+ - normalizeFootprint (function): function normalizeFootprint(footprint: Footprint): Footprint
1078
+ - rotateFootprint (function): function rotateFootprint(footprint: Footprint, rotation: Rotation): Footprint
1079
+ - occupiedCells (function): function occupiedCells(footprint: Footprint, origin: Cell, rotation: Rotation): Cell[]
1080
+ - createShapedGrid (function): function createShapedGrid<T>(width: number, height: number): ShapedGrid<T>
1081
+ - canPlace (function): function canPlace<T>(grid: ShapedGrid<T>, footprint: Footprint, origin: Cell, rotation: Rotation, ignoreId?: string): ShapedRejection | null
1082
+ - placeShaped (function): function placeShaped<T>(grid: ShapedGrid<T>, item: ShapedItem<T>, origin: Cell, rotation: Rotation = 0): ShapedResult<T>
1083
+ - removeShaped (function): function removeShaped<T>(grid: ShapedGrid<T>, id: string): ShapedResult<T>
1084
+ - moveShaped (function): function moveShaped<T>(grid: ShapedGrid<T>, id: string, origin: Cell, rotation?: Rotation): ShapedResult<T>
1085
+ - cellOccupant (function): function cellOccupant<T>(grid: ShapedGrid<T>, cell: Cell): string | null
1086
+ - gridAdjacencyQuery (function): function gridAdjacencyQuery<T>(grid: ShapedGrid<T>, options: { diagonal?: boolean } = {}): GridAdjacencyQuery
1087
+ - cellFromPoint (function): function cellFromPoint(point: { x: number; y: number }, cellSize: number, gridOrigin: { x: number; y: number } = { x: 0, y: 0 }): Cell
1088
+ - Cell (type): type Cell = readonly [number, number]
1089
+ - Footprint (type): type Footprint = readonly Cell[]
1090
+ - Rotation (type): type Rotation = 0 | 1 | 2 | 3
1091
+ - ShapedItem (interface): interface ShapedItem<T>
1092
+ - Placement (interface): interface Placement<T>
1093
+ - ShapedGrid (interface): interface ShapedGrid<T>
1094
+ - ShapedRejection (type): type ShapedRejection = "out-of-bounds" | "overlap" | "duplicate-id" | "unknown-id"
1095
+ - ShapedPlaceResult (interface): interface ShapedPlaceResult<T>
1096
+ - ShapedRejected (interface): interface ShapedRejected
1097
+ - ShapedResult (type): type ShapedResult<T> = ShapedPlaceResult<T> | ShapedRejected
1098
+ - GridAdjacencyQuery (interface): interface GridAdjacencyQuery
1099
+
1100
+ ### @jgengine/core/inventory/slotModel
1101
+
1102
+ - createSlots (function): function createSlots<T>(size: number): Slot<T>[]
1103
+ - normalizeSlots (function): function normalizeSlots<T>(values: SlotGrid<T>, size: number): Slot<T>[]
1104
+ - placeAt (function): function placeAt<T>(grid: SlotGrid<T>, index: number, value: T): SlotResult<T>
1105
+ - removeAt (function): function removeAt<T>(grid: SlotGrid<T>, index: number): SlotResult<T>
1106
+ - moveSlot (function): function moveSlot<T>(grid: SlotGrid<T>, from: number, to: number): SlotResult<T>
1107
+ - firstEmpty (function): function firstEmpty<T>(grid: SlotGrid<T>): number | null
1108
+ - insertFirst (function): function insertFirst<T>(grid: SlotGrid<T>, value: T): SlotResult<T>
1109
+ - compactSlots (function): function compactSlots<T>(grid: SlotGrid<T>): Slot<T>[]
1110
+ - countFilled (function): function countFilled<T>(grid: SlotGrid<T>): number
1111
+ - indexOfSlot (function): function indexOfSlot<T>(grid: SlotGrid<T>, match: (value: T, index: number) => boolean): number | null
1112
+ - Slot (type): type Slot<T> = T | null
1113
+ - SlotGrid (type): type SlotGrid<T> = readonly Slot<T>[]
1114
+ - SlotResult (type): type SlotResult<T> = | { status: "ok"; grid: Slot<T>[] } | { status: "rejected"; reason: "invalid-slot" | "slot-occupied" | "empty-slot" }
1115
+
1116
+ ### @jgengine/core/inventory/storageTier
1117
+
1118
+ - tierOf (function): function tierOf(tiers: Record<string, StorageTier>, inventoryId: string): StorageTier
1119
+ - partitionOnDeath (function): function partitionOnDeath(containers: readonly ContainerSnapshot[]): DeathPartition
1120
+ - createDeliveryQueue (function): function createDeliveryQueue(): DeliveryQueue
1121
+ - insureLost (function): function insureLost(lost: readonly ItemStack[], policy: InsurancePolicy, userId: string, now: number, rng: () => number = Math.random): ScheduledDelivery | null
1122
+ - resolveConsolation (function): function resolveConsolation(policy: ConsolationPolicy, partition: DeathPartition): { loadoutId: string } | null
1123
+ - StorageTier (type): type StorageTier = "carried" | "banked"
1124
+ - ItemStack (interface): interface ItemStack
1125
+ - ContainerSnapshot (interface): interface ContainerSnapshot
1126
+ - DeathPartition (interface): interface DeathPartition
1127
+ - DeliveryEntry (interface): interface DeliveryEntry
1128
+ - ScheduledDelivery (interface): interface ScheduledDelivery
1129
+ - DeliveryQueue (interface): interface DeliveryQueue
1130
+ - InsurancePolicy (interface): interface InsurancePolicy
1131
+ - ConsolationPolicy (interface): interface ConsolationPolicy
1132
+
1133
+ ### @jgengine/core/item/affix
1134
+
1135
+ - createAffixRoller (function): function createAffixRoller(config: RollerConfig): AffixRoller
1136
+ - AffixOp (type): type AffixOp = "add" | "mul"
1137
+ - AffixDef (interface): interface AffixDef
1138
+ - AffixPool (interface): interface AffixPool
1139
+ - RarityTier (interface): interface RarityTier
1140
+ - ItemBaseDef (interface): interface ItemBaseDef
1141
+ - RollerConfig (interface): interface RollerConfig
1142
+ - RolledAffix (interface): interface RolledAffix
1143
+ - RolledItem (interface): interface RolledItem
1144
+ - seededRng (function): function seededRng(seed: string | number): () => number
1145
+ - AffixRoller (interface): interface AffixRoller
1146
+
1147
+ ### @jgengine/core/item/durability
1148
+
1149
+ - createDurability (function): function createDurability(spec: DurabilitySpec): DurabilityState
1150
+ - durabilityFraction (function): function durabilityFraction(state: DurabilityState): number
1151
+ - isBroken (function): function isBroken(state: DurabilityState): boolean
1152
+ - isDisabled (function): function isDisabled(spec: DurabilitySpec, state: DurabilityState): boolean
1153
+ - wearAmount (function): function wearAmount(spec: DurabilitySpec, kind: WearKind): number
1154
+ - applyWear (function): function applyWear(state: DurabilityState, amount: number): DurabilityState
1155
+ - wear (function): function wear(spec: DurabilitySpec, state: DurabilityState, kind: WearKind, times = 1): DurabilityState
1156
+ - canRepairAt (function): function canRepairAt(spec: DurabilitySpec, stationId?: string): boolean
1157
+ - repairQuote (function): function repairQuote(spec: DurabilitySpec, state: DurabilityState, options?: { to?: number; station?: string }): RepairQuote | null
1158
+ - createDurabilityTracker (function): function createDurabilityTracker(): DurabilityTracker
1159
+ - RepairMaterial (interface): interface RepairMaterial
1160
+ - RepairSpec (interface): interface RepairSpec
1161
+ - DurabilitySpec (interface): interface DurabilitySpec
1162
+ - DurabilityState (interface): interface DurabilityState
1163
+ - WearKind (type): type WearKind = "use" | "hit"
1164
+ - RepairCost (interface): interface RepairCost
1165
+ - RepairQuote (interface): interface RepairQuote
1166
+ - DurabilityTracker (interface): interface DurabilityTracker
1167
+
1168
+ ### @jgengine/core/item/modularItem
1169
+
1170
+ - slotById (function): function slotById(def: ModularItemDef, slotId: string): MountSlotDef | null
1171
+ - slotAccepts (function): function slotAccepts(slot: MountSlotDef, category: string): boolean
1172
+ - install (function): function install(def: ModularItemDef, installed: readonly InstalledPart[], slotId: string, part: PartDef): InstallResult
1173
+ - uninstall (function): function uninstall(installed: readonly InstalledPart[], slotId: string): readonly InstalledPart[]
1174
+ - partInSlot (function): function partInSlot(installed: readonly InstalledPart[], slotId: string): PartDef | null
1175
+ - computeEffectiveStats (function): function computeEffectiveStats(def: ModularItemDef, installed: readonly InstalledPart[]): Record<string, number>
1176
+ - missingRequiredSlots (function): function missingRequiredSlots(def: ModularItemDef, installed: readonly InstalledPart[]): string[]
1177
+ - isComplete (function): function isComplete(def: ModularItemDef, installed: readonly InstalledPart[]): boolean
1178
+ - createModularItem (function): function createModularItem(def: ModularItemDef, initial: readonly InstalledPart[] = []): ModularItem
1179
+ - MountSlotDef (interface): interface MountSlotDef
1180
+ - ModularItemDef (interface): interface ModularItemDef
1181
+ - PartDef (interface): interface PartDef
1182
+ - InstalledPart (interface): interface InstalledPart
1183
+ - InstallResult (type): type InstallResult = | { status: "ok"; installed: readonly InstalledPart[] } | { status: "rejected"; reason: "unknown-slot" | "wrong-category" | "slot-occupied" }
1184
+ - ModularItem (interface): interface ModularItem
1185
+
1186
+ ### @jgengine/core/item/use
1187
+
1188
+ - createItemUse (function): function createItemUse<TState>(resolveUse: (itemId: string) => string | null | undefined): ItemUse<TState>
1189
+ - ItemUseInput (interface): interface ItemUseInput
1190
+ - ItemUseRejection (interface): interface ItemUseRejection
1191
+ - ItemUseResult (interface): interface ItemUseResult<TState>
1192
+ - ItemUseHandler (interface): interface ItemUseHandler<TState>
1193
+ - ItemUse (interface): interface ItemUse<TState>
1194
+
1195
+ ### @jgengine/core/item/weapon
1196
+
1197
+ - getWeaponStat (function): function getWeaponStat(entry: WeaponEntry | null | undefined, stat: string): number | null
1198
+ - createWeaponStats (function): function createWeaponStats(resolveEntry: (itemId: string) => WeaponEntry | null | undefined): WeaponStats
1199
+ - WeaponEntry (interface): interface WeaponEntry
1200
+ - WeaponStats (interface): interface WeaponStats
1201
+
1202
+ ### @jgengine/core/meta/changelog
1203
+
1204
+ - VERSION (const): const VERSION: "0.8.0"
1205
+ - ChangelogEntry (interface): interface ChangelogEntry
1206
+ - CHANGELOG (const): const CHANGELOG: Record<string, ChangelogEntry>
1207
+
1208
+ ### @jgengine/core/movement/avatarGait
1209
+
1210
+ - advanceGaitPhase (function): function advanceGaitPhase(phase: number, speedUnitsPerSec: number, dtSec: number, tuning: GaitTuning = DEFAULT_GAIT_TUNING): number
1211
+ - gaitBobOffset (function): function gaitBobOffset(phase: number, speedUnitsPerSec: number, tuning: GaitTuning = DEFAULT_GAIT_TUNING): number — Vertical hop per footfall — two peaks per stride cycle.
1212
+ - gaitSwayAngle (function): function gaitSwayAngle(phase: number, speedUnitsPerSec: number, tuning: GaitTuning = DEFAULT_GAIT_TUNING): number — Signed side-to-side lean alternating with each footfall.
1213
+ - GaitTuning (interface): interface GaitTuning
1214
+ - DEFAULT_GAIT_TUNING (const): const DEFAULT_GAIT_TUNING: GaitTuning
1215
+
1216
+ ### @jgengine/core/movement/cameraRig
1217
+
1218
+ - clampCameraPitch (function): function clampCameraPitch(pitch: number): number
1219
+ - resolveTargetEyeHeight (function): function resolveTargetEyeHeight(crouching: boolean, grounded: boolean): number
1220
+ - advanceEyeHeight (function): function advanceEyeHeight(currentEyeHeight: number, targetEyeHeight: number, deltaSeconds: number): number
1221
+ - resolveHeadBobRate (function): function resolveHeadBobRate(crouching: boolean, running: boolean): number
1222
+ - resolveHeadBobAmplitude (function): function resolveHeadBobAmplitude(crouching: boolean, running: boolean, grounded: boolean): number
1223
+ - advanceHeadBobTime (function): function advanceHeadBobTime(currentHeadBobTime: number, moving: boolean, grounded: boolean, bobRate: number, deltaSeconds: number): number
1224
+ - resolveHeadBobOffset (function): function resolveHeadBobOffset(headBobTime: number, bobAmplitude: number): number
1225
+ - resolveCameraTrackingBlend (function): function resolveCameraTrackingBlend(deltaSeconds: number): number
1226
+ - CAMERA_PITCH_LIMIT (const): const CAMERA_PITCH_LIMIT: 1.45
1227
+
1228
+ ### @jgengine/core/movement/dash
1229
+
1230
+ - iframeActive (function): function iframeActive(config: DashConfig, elapsedMs: number): boolean
1231
+ - dashEase (function): function dashEase(t: number): number
1232
+ - dashOffset (function): function dashOffset(config: DashConfig, dir: DashDirection, elapsedMs: number): [number, number, number]
1233
+ - createDashState (function): function createDashState(config: DashConfig): DashState
1234
+ - DashConfig (interface): interface DashConfig
1235
+ - DashDirection (interface): interface DashDirection
1236
+ - DashRejection (type): type DashRejection = { reason: "no-stamina" | "cooldown" | "dashing" }
1237
+ - DashBurst (interface): interface DashBurst
1238
+ - DashState (interface): interface DashState
1239
+
1240
+ ### @jgengine/core/movement/movementModel
1241
+
1242
+ - createEmptyMovementKeys (function): function createEmptyMovementKeys(): MovementKeysState
1243
+ - resolveMovementIntent (function): function resolveMovementIntent(keys: MovementKeysState, canMove: boolean): MovementIntent — Translate the set of held keys into an intent. When `canMove` is false (a menu is open, the world is paused) the avatar is fully idle so it never drifts behind an overlay.
1244
+ - createPlayerMotionState (function): function createPlayerMotionState(): PlayerMotionState
1245
+ - advancePlayerMotion (function): function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, tuning?: MovementTuningOverrides): MovementFrameStep — Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump, gravity, grounded) and returns the horizontal step to commit through the collision-resolving stepper.
1246
+ - resolveObstacleStep (function): function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius: number = DEFAULT_OBSTACLE_PLAYER_RADIUS): MovementFrameStep — Resolve a horizontal step against nearby placed objects with classic axis-separated sliding (try X, then Z against the post-X position) so walking into a wall stops the blocked axis but keeps sliding along the other. Obstacles are treated as circle-vs-AABB in the X/Z plane, inflated by `playerRadius`; an obstacle whose vertical span misses the player's feet-to-head span is skipped entirely. Callers should pre-filter to nearby objects — this also early-exits per obstacle on horizontal distance.
1247
+ - constrainStepToAxis (function): function constrainStepToAxis(stepX: number, stepZ: number, axis: "x" | "z"): MovementFrameStep — Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`).
1248
+ - snapPositionToGrid (function): function snapPositionToGrid(x: number, z: number, cellSize: number): [number, number] — Snap a world position to its containing cell's center, matching the bounds-free form of `navGrid`'s and `tacticalGrid`'s `floor` + half-cell convention (`PlayerMovementConfig.mode: "grid"`).
1249
+ - cameraYawToAvatarBodyYaw (function): function cameraYawToAvatarBodyYaw(cameraYaw: number): number — Camera yaw looks along -Z; character mesh faces +Z at body rotation.y = 0.
1250
+ - MovementKey (type): type MovementKey = "w" | "a" | "s" | "d" | "shift" | "control" | "c" | "space" — Movement core (pure model).
1251
+ - MovementKeysState (type): type MovementKeysState = Record<MovementKey, boolean>
1252
+ - MovementIntent (interface): interface MovementIntent — Frame-independent description of what the player is asking the avatar to do. `forward`/`right` are in the range -1..1 in the avatar's local frame; the controller rotates them into world space against the camera each frame.
1253
+ - MOVEMENT_TUNING (const): const MOVEMENT_TUNING: { readonly standEyeHeight: 1.7; readonly crouchEyeHeight: 1.15; readonly walkSpeedMultiplier: 1.75; readonly runSpeedMultiplier: 2.25; readonly crouchSpeedMultiplier: 0.45; readonly groundAcceleration: 26; readonly airAcceleration: 12; readonly groundFriction: 18; readonly jum… — Kinematics + feel tuning for the first-person controller. Centralised here so movement feel lives in one place rather than scattered through the renderer.
1254
+ - PlayerMotionState (interface): interface PlayerMotionState — Mutable kinematic state carried between frames by the controller. Kept here so the velocity / jump / gravity integration is a pure function testable without a renderer — the controller just owns the ref.
1255
+ - MovementFrameStep (interface): interface MovementFrameStep — Horizontal step (world units) the avatar should commit this frame.
1256
+ - MovementTuningOverrides (interface): interface MovementTuningOverrides — Per-game overrides for the gravity/jump feel, sourced from `GameDefinition.physics`. Omitted fields fall back to {@link MOVEMENT_TUNING}.
1257
+ - CollisionObstacle (interface): interface CollisionObstacle — A placed scene object the walking player collides against as a circle-vs-AABB obstacle.
1258
+ - MAX_JUMP_OFFSET (const): const MAX_JUMP_OFFSET: 1.15 — Peak jump height from MOVEMENT_TUNING.jumpVelocity + gravity, with small buffer.
1259
+
1260
+ ### @jgengine/core/movement/poseState
1261
+
1262
+ - createPoseState (function): function createPoseState(resolveAllowed: (instanceId: string) => PoseAllowedStates | null | undefined): PoseState
1263
+ - MovementPose (type): type MovementPose = "standing" | "crouch" | "prone" | "running"
1264
+ - AimMode (type): type AimMode = "hip" | "ads"
1265
+ - PoseHitbox (interface): interface PoseHitbox
1266
+ - POSE_HITBOX (const): const POSE_HITBOX: Record<MovementPose, PoseHitbox>
1267
+ - PoseAllowedStates (interface): interface PoseAllowedStates
1268
+ - PoseRejection (interface): interface PoseRejection
1269
+ - PoseState (interface): interface PoseState
1270
+
1271
+ ### @jgengine/core/movement/voxelController
1272
+
1273
+ - createVoxelPlayerBody (function): function createVoxelPlayerBody(x: number, y: number, z: number): VoxelPlayerBody
1274
+ - advanceVoxelPlayer (function): function advanceVoxelPlayer(body: VoxelPlayerBody, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, isSolid: SolidQuery, dims: VoxelPlayerDims = DEFAULT_VOXEL_DIMS, tuning?: MovementTuningOverrides, groundHeight?: GroundHeightQuery): void — Advance one frame of voxel-collided player kinematics. Mutates `body` (position, velocity, grounded) in place.
1275
+ - SolidQuery (type): type SolidQuery = (x: number, y: number, z: number) => boolean
1276
+ - VoxelPlayerDims (interface): interface VoxelPlayerDims — Footprint + height of the player box, in world units.
1277
+ - DEFAULT_VOXEL_DIMS (const): const DEFAULT_VOXEL_DIMS: VoxelPlayerDims
1278
+ - VoxelPlayerBody (interface): interface VoxelPlayerBody — Mutable kinematic state carried between frames by the voxel controller.
1279
+ - GroundHeightQuery (type): type GroundHeightQuery = (x: number, z: number) => number — Samples the terrain floor height at a point; unset means flat ground plays no part in support.
1280
+
1281
+ ### @jgengine/core/multiplayer/chatContract
1282
+
1283
+ - createLocalChatTransport (function): function createLocalChatTransport(options?: { userId?: string; historyLimit?: number; now?: () => number; }): { transport: ChatTransport; sync: ChatSync; actions: ChatActions; }
1284
+ - ChatSendArgs (interface): interface ChatSendArgs
1285
+ - ChatSendOutcome (interface): interface ChatSendOutcome
1286
+ - ChatActions (interface): interface ChatActions
1287
+ - ChatTransport (interface): interface ChatTransport — Backend seam for remote text chat, mirroring PresenceTransport: the use* members are called as React hooks by consumers, so a mounted transport must never change identity — remount the subtree to switch backends. useMessages returns undefined while the subscription is loading and the channel's recent history once live.
1288
+ - ChatSync (interface): interface ChatSync — Callback seam for backends that cannot host React hooks (e.g. the ws client): subscribe delivers the channel's recent history on every change; send resolves with the host's verdict.
1289
+
1290
+ ### @jgengine/core/multiplayer/combatSnapshot
1291
+
1292
+ - serializeBoard (function): function serializeBoard(args: SerializeBoardArgs): BoardSnapshot
1293
+ - cloneSnapshot (function): function cloneSnapshot(snapshot: BoardSnapshot): BoardSnapshot
1294
+ - replayCombat (function): function replayCombat(a: BoardSnapshot, b: BoardSnapshot, rules: CombatRules): ReplayResult
1295
+ - SnapshotUnit (interface): interface SnapshotUnit
1296
+ - BoardSnapshot (interface): interface BoardSnapshot
1297
+ - SerializeBoardArgs (interface): interface SerializeBoardArgs
1298
+ - CombatRules (interface): interface CombatRules
1299
+ - CombatSide (type): type CombatSide = "a" | "b" | "draw"
1300
+ - ReplayBlow (interface): interface ReplayBlow
1301
+ - ReplayResult (interface): interface ReplayResult
1302
+
1303
+ ### @jgengine/core/multiplayer/identity
1304
+
1305
+ - sessionPlayer (function): function sessionPlayer(session: AuthSession): PlayerIdentity
1306
+ - resolveGuestSession (function): function resolveGuestSession(seed?: string): AuthSession
1307
+ - AuthSession (interface): interface AuthSession
1308
+ - PlayerIdentity (interface): interface PlayerIdentity
1309
+
1310
+ ### @jgengine/core/multiplayer/lagCompensation
1311
+
1312
+ - createPositionHistory (function): function createPositionHistory(config: PositionHistoryConfig): PositionHistory
1313
+ - rewindTimestamp (function): function rewindTimestamp(nowMs: number, rttMs: number, interpDelayMs: number): number
1314
+ - raySphereDistance (function): function raySphereDistance(ray: HitscanRay, center: Vec3, radius: number): number | null
1315
+ - resolveHitscan (function): function resolveHitscan(history: PositionHistory, targets: readonly HitscanTarget[], ray: HitscanRay, atMs: number): HitscanHit | null
1316
+ - Vec3 (interface): interface Vec3
1317
+ - PositionSample (interface): interface PositionSample extends Vec3
1318
+ - PositionHistoryConfig (interface): interface PositionHistoryConfig
1319
+ - PositionHistory (class): class PositionHistory
1320
+ - HitscanRay (interface): interface HitscanRay
1321
+ - HitscanTarget (interface): interface HitscanTarget
1322
+ - HitscanHit (interface): interface HitscanHit
1323
+
1324
+ ### @jgengine/core/multiplayer/matchmaking
1325
+
1326
+ - normalizeJoinCode (function): function normalizeJoinCode(code: string): string
1327
+ - generateJoinCode (function): function generateJoinCode(random: () => number, length = 6): string
1328
+ - hasSpace (function): function hasSpace(listing: SessionListing): boolean
1329
+ - matchesFilter (function): function matchesFilter(listing: SessionListing, filter: MatchFilter): boolean
1330
+ - browseSessions (function): function browseSessions(listings: readonly SessionListing[], filter: MatchFilter = {}, options: BrowseOptions = {}): SessionListing[]
1331
+ - findByJoinCode (function): function findByJoinCode(listings: readonly SessionListing[], code: string): SessionListing | null
1332
+ - quickMatch (function): function quickMatch(listings: readonly SessionListing[], filter: MatchFilter = {}): SessionListing | null
1333
+ - SessionVisibility (type): type SessionVisibility = "public" | "private"
1334
+ - SessionStatus (type): type SessionStatus = "open" | "running" | "closed"
1335
+ - SessionListing (interface): interface SessionListing
1336
+ - MatchFilter (interface): interface MatchFilter
1337
+ - BrowseOptions (interface): interface BrowseOptions
1338
+
1339
+ ### @jgengine/core/multiplayer/poseSyncGate
1340
+
1341
+ - createPoseSyncGate (function): function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate
1342
+ - PlayerPose (interface): interface PlayerPose
1343
+ - PoseSyncTuning (interface): interface PoseSyncTuning
1344
+ - PoseSyncGate (interface): interface PoseSyncGate
1345
+
1346
+ ### @jgengine/core/multiplayer/presenceContract
1347
+
1348
+ - createLocalPresenceTransport (function): function createLocalPresenceTransport<TRow, TLocation, TGameId extends string = string>(): { transport: PresenceTransport<TRow, TLocation, TGameId>; actions: PresenceActions<TGameId>; }
1349
+ - PresencePosition (interface): interface PresencePosition
1350
+ - PresencePose (interface): interface PresencePose
1351
+ - PresenceSession (interface): interface PresenceSession<TGameId extends string = string>
1352
+ - EnsurePresenceArgs (interface): interface EnsurePresenceArgs<TGameId extends string = string>
1353
+ - EnsurePresenceResult (interface): interface EnsurePresenceResult
1354
+ - LeavePresenceArgs (interface): interface LeavePresenceArgs<TGameId extends string = string>
1355
+ - PresenceFeeds (interface): interface PresenceFeeds<TRow, TLocation>
1356
+ - PresenceActions (interface): interface PresenceActions<TGameId extends string = string>
1357
+ - PresenceTransport (interface): interface PresenceTransport<TRow, TLocation, TGameId extends string = string> — Backend seam for multiplayer presence. Feeds are reactive data and change identity whenever any player's pose updates; actions MUST be identity-stable for the lifetime of a mounted session so join/leave lifecycle effects can depend on them without re-running per pose tick. The use* members are called as React hooks by consumers, so a mounted transport must never change identity — remount the subtree to switch backends.
1358
+
1359
+ ### @jgengine/core/multiplayer/presenceModel
1360
+
1361
+ - decidePoseSync (function): function decidePoseSync(current: PresencePoseState, incoming: IncomingPose, rules: PoseSyncRules, nowMs: number): PoseSyncDecision
1362
+ - shouldRefreshKeepAlive (function): function shouldRefreshKeepAlive(lastSeenAtMs: number | undefined, nowMs: number, rules: Pick<PoseSyncRules, "keepAliveRefreshMs">): boolean
1363
+ - shouldPersistWorldSnapshot (function): function shouldPersistWorldSnapshot(lastSavedAtMs: number | undefined, nowMs: number, intervalMs: number): boolean
1364
+ - resolveActivePresence (function): function resolveActivePresence<T extends { revokedAt?: number; lastSeenAt?: number }>(rows: readonly T[]): ActivePresenceResolution<T>
1365
+ - isPresenceExpired (function): function isPresenceExpired(lastSeenAtMs: number, nowMs: number, idleCutoffMs: number): boolean
1366
+ - pickReusablePresence (function): function pickReusablePresence<T extends { lastSeenAt?: number }>(rows: readonly T[]): T | undefined — Most-recently-seen row across an actor's rows, to reuse instead of inserting a new one.
1367
+ - PresencePoseState (interface): interface PresencePoseState
1368
+ - IncomingPose (interface): interface IncomingPose
1369
+ - PoseSyncRules (interface): interface PoseSyncRules
1370
+ - PoseSyncDecision (interface): interface PoseSyncDecision
1371
+ - ActivePresenceResolution (interface): interface ActivePresenceResolution<T>
1372
+
1373
+ ### @jgengine/core/multiplayer/simultaneousCommit
1374
+
1375
+ - createCommitRound (function): function createCommitRound<TAction>(config: CommitRoundConfig): CommitRound<TAction>
1376
+ - resolveCommits (function): function resolveCommits<TAction, TResult>(reveals: readonly SealedCommit<TAction>[], resolver: (ordered: readonly SealedCommit<TAction>[]) => TResult): TResult
1377
+ - CommitPhase (type): type CommitPhase = "collecting" | "revealed"
1378
+ - SealedCommit (interface): interface SealedCommit<TAction>
1379
+ - CommitRoundConfig (interface): interface CommitRoundConfig
1380
+ - SealResult (type): type SealResult = | { ok: true; allSealed: boolean } | { ok: false; reason: "unknown_participant" | "already_sealed" | "already_revealed" }
1381
+ - CommitRound (class): class CommitRound<TAction>
1382
+
1383
+ ### @jgengine/core/multiplayer/voiceContract
1384
+
1385
+ - createLocalVoiceTransport (function): function createLocalVoiceTransport(options?: { userId?: string }): { transport: VoiceTransport; participants(channelId: string): readonly VoiceParticipant[]; }
1386
+ - createPushToTalk (function): function createPushToTalk(config?: { mode?: PushToTalkMode; onChange?: (transmitting: boolean) => void; }): PushToTalk
1387
+ - VoiceParticipant (interface): interface VoiceParticipant
1388
+ - VoiceRoute (interface): interface VoiceRoute
1389
+ - VoiceTransport (interface): interface VoiceTransport — Signaling seam for voice: who is in a channel and which media stream descriptor they published. The media plane (WebRTC, SFU, or anything else that moves audio bytes) stays behind this seam, host-supplied — the engine never touches it. subscribers delivers the channel roster on every change, starting with the current roster.
1390
+ - PushToTalkMode (type): type PushToTalkMode = "hold" | "toggle" | "openMic"
1391
+ - PushToTalkStatus (type): type PushToTalkStatus = "idle" | "keyed" | "open"
1392
+ - PushToTalk (interface): interface PushToTalk
1393
+
1394
+ ### @jgengine/core/nav/navConstrain
1395
+
1396
+ - constrainToNavGrid (function): function constrainToNavGrid(grid: NavGrid, options?: NavConstrainOptions): (proposed: NavConstrainProposed, entity: NavConstrainEntity) => NavConstrainProposed | null
1397
+ - NavConstrainProposed (interface): interface NavConstrainProposed
1398
+ - NavConstrainEntity (interface): interface NavConstrainEntity
1399
+ - NavConstrainOptions (interface): interface NavConstrainOptions
1400
+
1401
+ ### @jgengine/core/nav/navFromEnvironment
1402
+
1403
+ - populateNavGridFromEnvironment (function): function populateNavGridFromEnvironment(grid: NavObstacleGrid, world: EnvironmentWorldFeature): number — Expands every structure descriptor on an environment world feature into its generated buildings and blocks their footprints on `grid`. Returns the number of buildings blocked.
1404
+ - NavObstacleGrid (interface): interface NavObstacleGrid
1405
+
1406
+ ### @jgengine/core/nav/navGrid
1407
+
1408
+ - createNavGrid (function): function createNavGrid(config: NavGridConfig): NavGrid
1409
+ - findPath (function): function findPath(grid: NavGrid, from: NavPoint, to: NavPoint, options: FindPathOptions = {}): NavPoint[] | null — A* over the walkable grid. Returns a polyline of world-space `[x, z]` waypoints from `from` to `to`, or `null` when no route exists. Blocked start/goal snap to the nearest walkable cell so a click on an obstacle still routes to its edge.
1410
+ - slopeStepCost (function): function slopeStepCost(field: { sampleHeight(x: number, z: number): number }, weight = DEFAULT_SLOPE_STEP_WEIGHT): (from: NavPoint, to: NavPoint) => number — `FindPathOptions.stepCost` factory that penalizes steep terrain: cost is `1 + weight * |Δheight| / horizontalDistance`, so with the default weight a 45° slope roughly doubles the step cost.
1411
+ - smoothPath (function): function smoothPath(grid: NavGrid, points: readonly NavPoint[]): NavPoint[] — Remove waypoints the mover can skip because it has clear line-of-sight past them.
1412
+ - NavPoint (type): type NavPoint = readonly [number, number]
1413
+ - NavGridConfig (interface): interface NavGridConfig
1414
+ - NavCell (interface): interface NavCell
1415
+ - FindPathOptions (interface): interface FindPathOptions
1416
+ - NavGrid (interface): interface NavGrid
1417
+
1418
+ ### @jgengine/core/nav/pathFollow
1419
+
1420
+ - pathFromNav (function): function pathFromNav(points: readonly NavPoint[], elevation: number | HeightSampler = 0, offset = 0): Waypoint[]
1421
+ - createPathFollow (function): function createPathFollow(config: PathFollowConfig): PathFollowState
1422
+ - advancePathFollow (function): function advancePathFollow(config: PathFollowConfig, state: PathFollowState, dt: number): PathFollowState — Advance a path-follower by `speed * dt` along its authored polyline. Pure — returns the next state. Crosses multiple waypoints in one step, loops when configured, and reports `done` at the end of a non-looping path. No navmesh required (#52); feed it a navmesh route via `pathFromNav` for click-to-move (#51).
1423
+ - Waypoint (type): type Waypoint = readonly [number, number, number]
1424
+ - PathFollowConfig (interface): interface PathFollowConfig
1425
+ - PathFollowState (interface): interface PathFollowState
1426
+ - HeightSampler (interface): interface HeightSampler
1427
+
1428
+ ### @jgengine/core/physics/ballisticSweep
1429
+
1430
+ - createBallisticSweep (function): function createBallisticSweep(world: PhysicsWorld, options: BallisticSweepOptions = {}): BallisticSweep — Marches the closed-form arc (constant gravity, straight lateral) through `world` and reports the first sample inside any live body's AABB — sleeping bodies included — refined by one bisection between the last clear sample and the hit sample. Returns `null` when the whole arc is clear.
1431
+ - BallisticSweepHit (interface): interface BallisticSweepHit
1432
+ - BallisticSweep (type): type BallisticSweep = ( origin: readonly [number, number, number], velocity: readonly [number, number, number], gravity: number, maxTime: number, ) => BallisticSweepHit | null
1433
+ - BallisticSweepOptions (interface): interface BallisticSweepOptions
1434
+
1435
+ ### @jgengine/core/physics/buoyancy
1436
+
1437
+ - createBuoyantBody (function): function createBuoyantBody(world: PhysicsWorld, config: BuoyantBodyConfig): BuoyantBody
1438
+ - BuoyantBodyConfig (interface): interface BuoyantBodyConfig
1439
+ - BuoyantBody (class): class BuoyantBody — Floats a {@link PhysicsWorld} body on a CPU {@link WaterSurface}: each hull sample point pushes the body up by its submerged depth (Archimedes, coarse), with vertical and horizontal water drag so the hull settles at the waterline and rides the Gerstner waves. Passing an {@link AxisInput} drives it as a boat — throttle thrusts along the heading, steer yaws, a keel bleeds sideways slip. Call `update(dt, time, input?)` before the shared `world.step(dt)`.
1440
+
1441
+ ### @jgengine/core/physics/carryable
1442
+
1443
+ - carrySpeedMultiplier (function): function carrySpeedMultiplier(mass: number, carryCapacity: number, owners: number): number — Movement multiplier (1 = unhindered, →0 = crushed) for a body of `mass` carried by `owners`. Pure — the HUD/movement kit reads it to slow a laden hauler (Lethal Company) and to gate items that need 2+ people (R.E.P.O.).
1444
+ - CarryableConfig (interface): interface CarryableConfig
1445
+ - Carryable (class): class Carryable — A grabbed physics object following a moving hold point through a spring constraint (the pick — a raycast — is the caller's/shell's job; core owns the constraint). Supports shared multi-owner carry (the follow point is the average of owners' hold points), an encumbrance read, and drop/throw. Reuses `PhysicsWorld.springJoint` to a world anchor moved each frame.
1446
+
1447
+ ### @jgengine/core/physics/damageZones
1448
+
1449
+ - createDamageModel (function): function createDamageModel(config: DamageModelConfig): DamageModel
1450
+ - DamageZoneDef (interface): interface DamageZoneDef
1451
+ - DamageZoneState (interface): interface DamageZoneState
1452
+ - DamageTransition (interface): interface DamageTransition
1453
+ - DamageModelConfig (interface): interface DamageModelConfig
1454
+ - DamageModel (class): class DamageModel — Coarse crash-damage model (issue #86 — stages, not soft-body). Each zone accumulates contact impulse fed from G3's `onCollision` `CollisionEvent`; crossing a threshold bumps the zone to the next discrete stage (the caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once, and a total-impulse `disableAt` flips the whole body to a disabled state. `absorb` returns a transition only when something actually changed, so it doubles as the event source for HUD/feed.
1455
+
1456
+ ### @jgengine/core/physics/forceVolume
1457
+
1458
+ - ForceMode (type): type ForceMode = "impulse" | "velocity" | "accelerate"
1459
+ - ForceVolumeConfig (interface): interface ForceVolumeConfig
1460
+ - ForceVolume (class): class ForceVolume — A trigger region that pushes bodies passing through it — boost pads (`impulse` + `once`), conveyors (`velocity`), fans/wind (`accelerate`). Call `apply` each tick; `once` mode fires only on entry by tracking membership between ticks.
1461
+ - PlatformCarryConfig (interface): interface PlatformCarryConfig
1462
+ - PlatformCarry (class): class PlatformCarry — Carries bodies standing on a moving platform by composing their transform with the platform's per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The platform is a body the game repositions each frame; riders are detected by overlap on its top face.
1463
+
1464
+ ### @jgengine/core/physics/physicsWorld
1465
+
1466
+ - cellCoord (function): function cellCoord(value: number, min: number, cellSize: number, cells: number): number — Grid cell containing a point, clamped into the grid. Pure — exported for tests.
1467
+ - cellIndex (function): function cellIndex(cx: number, cy: number, cz: number, nx: number, ny: number): number — Linear cell index from clamped 3D coords. Pure — exported for tests.
1468
+ - PhysicsBounds (interface): interface PhysicsBounds
1469
+ - PhysicsWorldConfig (interface): interface PhysicsWorldConfig
1470
+ - PhysicsStats (interface): interface PhysicsStats
1471
+ - BodyCommonOptions (interface): interface BodyCommonOptions
1472
+ - BoxBodyOptions (interface): interface BoxBodyOptions extends BodyCommonOptions
1473
+ - SphereBodyOptions (interface): interface SphereBodyOptions extends BodyCommonOptions — The radius fills all three half-extent columns, so broadphase and bounds see the sphere's enclosing AABB.
1474
+ - AddBodyOptions (type): type AddBodyOptions = BoxBodyOptions | SphereBodyOptions
1475
+ - SHAPE_BOX (const): const SHAPE_BOX: 0
1476
+ - SHAPE_SPHERE (const): const SHAPE_SPHERE: 1
1477
+ - JointKind (type): type JointKind = "hinge" | "distance" | "spring" | "fixed" — `hinge`/`fixed`/`distance` are hard bilateral constraints; `spring` is a soft PD constraint. No angular state → `hinge`/`fixed` both pin the shared anchor (`axis` is metadata), `distance` holds a fixed separation, `spring` drives toward `restLength` (suspension, follow-point carry).
1478
+ - JointOptions (interface): interface JointOptions
1479
+ - CollisionEvent (interface): interface CollisionEvent — A contact reported to `onCollision`. The object is reused each call — read/copy, never retain.
1480
+ - MAX_BROADPHASE_CELLS (const): const MAX_BROADPHASE_CELLS: 1000000 — Cap on `nx*ny*nz` broadphase cells — guards a huge-bounds/tiny-cellSize config from hanging `step()`.
1481
+ - PhysicsWorld (class): class PhysicsWorld
1482
+
1483
+ ### @jgengine/core/physics/ragdoll
1484
+
1485
+ - createRagdoll (function): function createRagdoll(world: PhysicsWorld, config: RagdollConfig): Ragdoll
1486
+ - RagdollBoneSpec (interface): interface RagdollBoneSpec
1487
+ - RagdollLinkSpec (interface): interface RagdollLinkSpec
1488
+ - RagdollBalanceConfig (interface): interface RagdollBalanceConfig
1489
+ - RagdollConfig (interface): interface RagdollConfig
1490
+ - Ragdoll (class): class Ragdoll — A jointed multi-body character — floppy by default, or active-ragdoll when a balance motor is configured. Bones are `PhysicsWorld` bodies; links are joints on the joint API. `balance(dt, mx, mz)` drives the root toward its target height (staying upright) and pushes it by the move vector.
1491
+
1492
+ ### @jgengine/core/physics/spatialGrid
1493
+
1494
+ - SpatialGridConfig (interface): interface SpatialGridConfig
1495
+ - SpatialGrid (class): class SpatialGrid — A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false positives beyond the exact distance test.
1496
+
1497
+ ### @jgengine/core/physics/structure
1498
+
1499
+ - StructureMaterial (interface): interface StructureMaterial
1500
+ - StructureMaterialTable (type): type StructureMaterialTable = Readonly<Record<string, StructureMaterial>>
1501
+ - StructureNodeSpec (interface): interface StructureNodeSpec
1502
+ - StructureEdgeSpec (interface): interface StructureEdgeSpec
1503
+ - CollapseEvent (interface): interface CollapseEvent
1504
+ - DebrisConfig (interface): interface DebrisConfig
1505
+ - StructureGraph (class): class StructureGraph — A structural-integrity graph over a building — nodes are pieces (walls, beams, floors), edges are load-bearing connections, some nodes are anchored foundations. `damage`/`damageEdge` wear pieces and connections down; when a piece shatters or an edge severs, the graph recomputes which pieces still reach an anchor and hands back every newly-disconnected piece as one `CollapseEvent`. Feed that to `toDebris` to sink the fallen pieces into a `PhysicsWorld` as rigid bodies ("The Finals" smooth destruction, Rainbow Six walls). Coarse by design: it replicates the collapse event, not per fragment.
1506
+
1507
+ ### @jgengine/core/physics/traversal
1508
+
1509
+ - GrappleConfig (interface): interface GrappleConfig
1510
+ - Grapple (class): class Grapple — A fired-anchor rope on the joint API — grapple (reel toward a hit point), zipline (rigid cable to a far anchor you then slide/reel along), swing (rigid rope + gravity = a pendulum). `fire` attaches a `distance`/`spring` joint from the traveller body to a fixed world point; `reel` shrinks its rest length so the constraint drags the body in; `moveAnchor` re-points it (zipline glide, grapple-to- moving-target). The pick — a raycast to find the anchor — is the caller's; core owns the constraint.
1511
+ - GlideConfig (interface): interface GlideConfig
1512
+ - Glide (class): class Glide — A reduced-gravity, forward-thrust glide over a physics body — wingsuit / glider / paraglider (Enshrouded, Grounded). Call `apply(dt, steerX, steerZ)` each frame *before* `world.step`: it feeds back most of the gravity the sim is about to apply (leaving `gravityScale` of it), pushes the body along the steer vector by `thrust`, and clamps descent to `maxFallSpeed`. Stop calling it to fall normally again — no attach/detach state to leak.
1513
+
1514
+ ### @jgengine/core/physics/vehicleBody
1515
+
1516
+ - sampleGripCurve (function): function sampleGripCurve(curve: GripCurve, slip: number): number — Piecewise-linear tire-grip curve: normalized lateral slip → available grip (0..1). Grip peaks near the breakaway slip then falls off as the tire slides — the shape that separates a planted corner from a drift. Points are read in ascending slip order; ends clamp.
1517
+ - createVehicleBody (function): function createVehicleBody(world: PhysicsWorld, config: VehicleBodyConfig): VehicleBody
1518
+ - GripCurve (interface): interface GripCurve
1519
+ - DEFAULT_GRIP_CURVE (const): const DEFAULT_GRIP_CURVE: GripCurve
1520
+ - WheelSpec (interface): interface WheelSpec
1521
+ - WheelState (interface): interface WheelState
1522
+ - VehicleBodyConfig (interface): interface VehicleBodyConfig
1523
+ - VehicleBody (class): class VehicleBody — Arcade vehicle over the {@link PhysicsWorld} rigid-body sim. The chassis is a single box body driven by an {@link AxisInput}: per-wheel suspension is a spring-damper held with G3's `springJoint` against the sampled ground, drive/brake push along the heading, and a tire-grip curve bleeds lateral velocity (cornering, and drift under handbrake). Because the chassis is a physics body it still collides — that contact feeds crash damage. Call `update(dt, input)` before the shared `world.step(dt)`.
1524
+
1525
+ ### @jgengine/core/puzzle/cellGrid
1526
+
1527
+ - createCellGrid (function): function createCellGrid<T>(width: number, height: number): CellGrid<T>
1528
+ - inGridBounds (function): function inGridBounds<T>(grid: CellGrid<T>, x: number, y: number): boolean
1529
+ - cellAt (function): function cellAt<T>(grid: CellGrid<T>, x: number, y: number): T | null
1530
+ - withCell (function): function withCell<T>(grid: CellGrid<T>, x: number, y: number, value: T | null): CellGrid<T>
1531
+ - withCells (function): function withCells<T>(grid: CellGrid<T>, entries: readonly { readonly x: number; readonly y: number; readonly value: T | null }[]): CellGrid<T>
1532
+ - fullRows (function): function fullRows<T>(grid: CellGrid<T>): number[]
1533
+ - clearRows (function): function clearRows<T>(grid: CellGrid<T>, rows: readonly number[]): CellGrid<T>
1534
+ - collapseColumns (function): function collapseColumns<T>(grid: CellGrid<T>): CellGrid<T>
1535
+ - findRuns (function): function findRuns<T>(grid: CellGrid<T>, minLength: number, matches: (a: T, b: T) => boolean = (a, b) => a === b): CellRun<T>[]
1536
+ - CellGrid (interface): interface CellGrid<T>
1537
+ - CellRun (interface): interface CellRun<T>
1538
+
1539
+ ### @jgengine/core/puzzle/fallingPiece
1540
+
1541
+ - pieceCells (function): function pieceCells<TShape extends string>(table: ShapeTable<TShape>, piece: FallingPiece<TShape>): readonly (readonly [number, number])[]
1542
+ - pieceCollides (function): function pieceCollides<T, TShape extends string>(grid: CellGrid<T>, table: ShapeTable<TShape>, piece: FallingPiece<TShape>): boolean
1543
+ - mergePiece (function): function mergePiece<T, TShape extends string>(grid: CellGrid<T>, table: ShapeTable<TShape>, piece: FallingPiece<TShape>, value: T): CellGrid<T>
1544
+ - dropDistance (function): function dropDistance<T, TShape extends string>(grid: CellGrid<T>, table: ShapeTable<TShape>, piece: FallingPiece<TShape>): number
1545
+ - gravityInterval (function): function gravityInterval(level: number, base = 0.8, perLevel = 0.07, min = 0.05): number
1546
+ - levelForLines (function): function levelForLines(lines: number, linesPerLevel = 10): number
1547
+ - lineScore (function): function lineScore(cleared: number, level: number, table: readonly number[] = CLASSIC_LINE_SCORE): number
1548
+ - createLockDelay (function): function createLockDelay(delaySeconds: number): LockDelayState
1549
+ - stepLockDelay (function): function stepLockDelay(state: LockDelayState, grounded: boolean, dt: number): { state: LockDelayState; locked: boolean }
1550
+ - FallingPiece (interface): interface FallingPiece<TShape extends string = string>
1551
+ - ShapeTable (type): type ShapeTable<TShape extends string = string> = Record< TShape, readonly (readonly (readonly [number, number])[])[] >
1552
+ - LockDelayState (interface): interface LockDelayState
1553
+
1554
+ ### @jgengine/core/random/nameGen
1555
+
1556
+ - pickFrom (function): function pickFrom(rng: () => number, bank: readonly string[]): string
1557
+ - createNameGenerator (function): function createNameGenerator(options: NameGeneratorOptions): NameGenerator
1558
+ - fillTemplate (function): function fillTemplate(template: string, vars: Record<string, () => string> | Record<string, string>): string
1559
+ - SyllableBank (interface): interface SyllableBank
1560
+ - NameGeneratorOptions (interface): interface NameGeneratorOptions
1561
+ - NameGenerator (interface): interface NameGenerator
1562
+
1563
+ ### @jgengine/core/random/rng
1564
+
1565
+ - seededRng (function): function seededRng(seed: string | number): () => number
1566
+ - seededStreams (function): function seededStreams(seed: string | number): (stream: string) => () => number
1567
+
1568
+ ### @jgengine/core/runtime/adapter
1569
+
1570
+ - convex (function): function convex(config?: { topology?: MultiplayerTopology }): MultiplayerAdapterConfig
1571
+ - ws (function): function ws(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig
1572
+ - fly (function): function fly(config: { app: string; topology?: MultiplayerTopology; path?: string }): MultiplayerAdapterConfig
1573
+ - socketIo (function): function socketIo(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig
1574
+ - p2p (function): function p2p(config?: { topology?: MultiplayerTopology; room?: string }): MultiplayerAdapterConfig
1575
+ - lan (function): function lan(config?: { topology?: MultiplayerTopology; port?: number; path?: string; }): MultiplayerAdapterConfig
1576
+ - offline (function): function offline(): MultiplayerAdapterConfig
1577
+ - adapterOf (function): function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null
1578
+ - multiplayerAdapterKind (function): function multiplayerAdapterKind(multiplayer: unknown): string | null
1579
+ - servers (function): function servers(config: ServersPoolConfig): import("/home/runner/work/jgengine/jgengine/packages/core/src/runtime/adapter").ServersPoolConfig
1580
+ - MultiplayerTopology (type): type MultiplayerTopology = "shared" | "lobbies" | "private"
1581
+ - MultiplayerAdapterConfig (type): type MultiplayerAdapterConfig = | { kind: "convex"; topology?: MultiplayerTopology } | { kind: "ws"; topology?: MultiplayerTopology; url?: string } | { kind: "socketio"; topology?: MultiplayerTopology; url?: string } | { kind: "p2p"; topology?: MultiplayerTopology; room?: string } | { kind: "lan"; t…
1582
+ - ServersPoolConfig (type): type ServersPoolConfig = { maxServers: number; slotsPerServer: number; minPlayersToStart?: number; adapter: MultiplayerAdapterConfig; }
1583
+
1584
+ ### @jgengine/core/runtime/cameraDirector
1585
+
1586
+ - createCameraDirector (function): function createCameraDirector(): CameraDirector
1587
+ - CameraDirector (interface): interface CameraDirector
1588
+
1589
+ ### @jgengine/core/runtime/commandRunner
1590
+
1591
+ - runCommand (function): function runCommand<TInput>(snapshot: import("./snapshot").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): RunCommandResult
1592
+ - CommandValidationError (type): type CommandValidationError = { reason: string }
1593
+ - CommandDef (type): type CommandDef<TInput = unknown> = { validate: (snapshot: import("./snapshot").GameRuntimeSnapshot, input: TInput) => CommandValidationError | null; apply: ( snapshot: import("./snapshot").GameRuntimeSnapshot, input: TInput, ) => import("./snapshot").GameRuntimeSnapshot; }
1594
+ - RunCommandResult (type): type RunCommandResult = | { ok: true; snapshot: import("./snapshot").GameRuntimeSnapshot } | { ok: false; reason: string }
1595
+
1596
+ ### @jgengine/core/runtime/gameContext
1597
+
1598
+ - createGameContext (function): function createGameContext<TAssetRef extends ModelAssetRef, TMultiplayer>(options: GameContextOptions<TAssetRef, TMultiplayer>): GameContext
1599
+ - GameContextItemEntry (interface): interface GameContextItemEntry
1600
+ - CatalogEntityRole (type): type CatalogEntityRole = "player" | "enemy" | "hostile" | "npc" | "vehicle"
1601
+ - GameContextEntityEntry (interface): interface GameContextEntityEntry
1602
+ - GameContextObjectEntry (interface): interface GameContextObjectEntry
1603
+ - GameContextContent (interface): interface GameContextContent
1604
+ - GameContextOptions (interface): interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown>
1605
+ - SceneObjectContext (interface): interface SceneObjectContext extends ObjectStore
1606
+ - FloatTextInput (interface): interface FloatTextInput
1607
+ - TelegraphInput (interface): interface TelegraphInput
1608
+ - HitReactionInput (interface): interface HitReactionInput
1609
+ - SceneEntityContext (interface): interface SceneEntityContext
1610
+ - WorldItemPickupResult (type): type WorldItemPickupResult = | { status: "ok"; record: WorldItemRecord } | { status: "rejected"; reason: string }
1611
+ - SceneWorldItemContext (interface): interface SceneWorldItemContext
1612
+ - GameContextCommands (interface): interface GameContextCommands
1613
+ - GameContextFeed (interface): interface GameContextFeed extends Omit<GameFeed, "bind">
1614
+ - GameContextLoot (interface): interface GameContextLoot
1615
+ - GameContextEconomy (interface): interface GameContextEconomy
1616
+ - GameContextItemUse (interface): interface GameContextItemUse
1617
+ - GameContextWorld (interface): interface GameContextWorld
1618
+ - GameContextCards (interface): interface GameContextCards
1619
+ - GameContextTurn (interface): interface GameContextTurn
1620
+ - GameContext (interface): interface GameContext
1621
+
1622
+ ### @jgengine/core/runtime/gameRuntime
1623
+
1624
+ - createGameRuntime (function): function createGameRuntime(definition: GameRuntimeDefinition): GameRuntime
1625
+ - ServerLoopHooks (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeLoopContext, dtSeconds: number) => void; }
1626
+ - RuntimeInitContext (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; }
1627
+ - RuntimeLoopContext (type): type RuntimeLoopContext = RuntimeInitContext & { player: { userId: string; isNew: boolean; }; }
1628
+ - GameRuntimeDefinition (type): type GameRuntimeDefinition = { gameId: string; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; }
1629
+ - HydrateInput (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; }
1630
+ - GameRuntime (type): type GameRuntime = { gameId: string; save: SaveConfig; hydrate: (input: HydrateInput) => GameRuntimeSnapshot; runCommand: ( snapshot: GameRuntimeSnapshot, actorUserId: string, commandName: string, input: unknown, ) => ReturnType<typeof runCommand>; tick: (snapshot: GameRuntimeSnapshot, dtSeconds: nu…
1631
+
1632
+ ### @jgengine/core/runtime/hostPersistence
1633
+
1634
+ - drainPendingLeaderboardIncrements (function): function drainPendingLeaderboardIncrements(session: Record<string, unknown>): { increments: LeaderboardIncrement[]; session: Record<string, unknown>; }
1635
+ - leaderboardRowKey (function): function leaderboardRowKey(row: Omit<LeaderboardRow, "value" | "updatedAt">): string
1636
+ - applyLeaderboardRows (function): function applyLeaderboardRows(rows: Map<string, LeaderboardRow>, gameId: string, entries: LeaderboardIncrement[], now: number): void
1637
+ - topLeaderboardRows (function): function topLeaderboardRows(rows: Iterable<LeaderboardRow>, args: { gameId: string; stat: string; scope: LeaderboardScope; serverId?: string; limit?: number; }): LeaderboardEntry[]
1638
+ - profileLeaderboardStats (function): function profileLeaderboardStats(rows: Iterable<LeaderboardRow>, gameId: string, userId: string): Record<string, number>
1639
+ - trimFeedEntries (function): function trimFeedEntries<T>(entries: T[], limit = FEED_RING_LIMIT): T[]
1640
+ - shouldAutoSave (function): function shouldAutoSave(save: SaveConfig, dirtyAt: number | undefined, lastSavedAt: number | undefined, now: number): boolean
1641
+ - toServerListing (function): function toServerListing(record: GameServerRecord): ServerListing
1642
+ - toOpenServerListings (function): function toOpenServerListings(listings: Iterable<ServerListing>, limit = 20): ServerListing[]
1643
+ - buildHydratePlayers (function): function buildHydratePlayers(server: GameServerRecord, profiles: Record<string, PlayerProfileRecord | null>): Record<string, RuntimePlayerRow>
1644
+ - planServerPersist (function): function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan
1645
+ - GameServerStatus (type): type GameServerStatus = "open" | "running" | "closed"
1646
+ - SessionVisibility (type): type SessionVisibility = "public" | "private"
1647
+ - SessionAttributes (type): type SessionAttributes = { label?: string; mode?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; }
1648
+ - GameServerRecord (type): type GameServerRecord = { serverId: string; gameId: string; status: GameServerStatus; mode?: string; modeConfig?: unknown; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; memberUserIds: string[]; slotsPerServer: number; save: SaveConfig; serverState: RuntimeServer…
1649
+ - PlayerProfileRecord (type): type PlayerProfileRecord = { userId: string; gameId: string; playerState: RuntimePlayerRow; revision: number; updatedAt: number; }
1650
+ - WorldChunkRecord (type): type WorldChunkRecord = { serverId: string; chunkKey: string; snapshot: RuntimeChunkRow; updatedAt: number; }
1651
+ - LeaderboardIncrement (type): type LeaderboardIncrement = { userId: string; stat: string; scope: LeaderboardScope; serverId?: string; by: number; }
1652
+ - LeaderboardEntry (type): type LeaderboardEntry = { userId: string; value: number; }
1653
+ - LEADERBOARD_PENDING_KEY (const): const LEADERBOARD_PENDING_KEY: "leaderboardPending"
1654
+ - LEADERBOARD_TOP_LIMIT (const): const LEADERBOARD_TOP_LIMIT: 100
1655
+ - LeaderboardRow (type): type LeaderboardRow = { gameId: string; stat: string; scope: LeaderboardScope; serverId?: string; userId: string; value: number; updatedAt: number; }
1656
+ - FEED_RING_LIMIT (const): const FEED_RING_LIMIT: 20
1657
+ - ServerListing (type): type ServerListing = { serverId: string; status: GameServerStatus; memberCount: number; slotsPerServer: number; mode?: string; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; updatedAt: number; }
1658
+ - HostPersistence (type): type HostPersistence = { savePlan?: (plan: ServerPersistPlan) => Promise<void>; resetScenario?: (reset: NormalizedScenarioReset) => Promise<void>; loadServer: (serverId: string) => Promise<GameServerRecord | null>; saveServer: (record: GameServerRecord) => Promise<void>; listServers: (gameId: string…
1659
+ - ServerPersistPlan (type): type ServerPersistPlan = { server: GameServerRecord; profiles: PlayerProfileRecord[]; chunks: WorldChunkRecord[]; leaderboard: LeaderboardIncrement[]; }
1660
+
1661
+ ### @jgengine/core/runtime/inputSnapshot
1662
+
1663
+ - createInputSnapshot (function): function createInputSnapshot(): InputSnapshot
1664
+ - InputSnapshot (interface): interface InputSnapshot
1665
+
1666
+ ### @jgengine/core/runtime/motionIntents
1667
+
1668
+ - createMotionIntents (function): function createMotionIntents(): MotionIntents
1669
+ - MotionIntents (interface): interface MotionIntents — Seam for game code to reach the vertical motion the shell's FrameDriver otherwise owns privately (#162.4). Game code calls `impulse`, `setVerticalVelocity`, and/or `setY` from `onTick` or commands; the shell calls `takePending()` once per frame, before integrating gravity, to drain what accumulated. `setY` wins over physics for that frame; impulses add to the vertical velocity the driver is about to integrate; a later `setVerticalVelocity` replaces that velocity outright.
1670
+ - MotionIntentBatch (interface): interface MotionIntentBatch
1671
+
1672
+ ### @jgengine/core/runtime/persistenceScope
1673
+
1674
+ - partitionScopes (function): function partitionScopes<T extends Record<string, unknown>>(state: T, schema: ScopeSchema): ScopedState<T>
1675
+ - resetRun (function): function resetRun<T extends Record<string, unknown>>(scoped: ScopedState<T>): ScopedState<T>
1676
+ - mergeScopes (function): function mergeScopes<T extends Record<string, unknown>>(scoped: ScopedState<T>): Partial<T>
1677
+ - clearRunFields (function): function clearRunFields(player: RuntimePlayerRow, runFields: readonly string[]): RuntimePlayerRow
1678
+ - applyRunReset (function): function applyRunReset(profile: PlayerProfileRecord, runFields: readonly string[], now: number): PlayerProfileRecord
1679
+ - planScenarioReset (function): function planScenarioReset(reset: ScenarioReset): NormalizedScenarioReset
1680
+ - PersistenceScope (type): type PersistenceScope = "run" | "meta"
1681
+ - ScopeSchema (interface): interface ScopeSchema
1682
+ - ScopedState (interface): interface ScopedState<T = Record<string, unknown>>
1683
+ - ScenarioReset (interface): interface ScenarioReset
1684
+ - NormalizedScenarioReset (interface): interface NormalizedScenarioReset
1685
+
1686
+ ### @jgengine/core/runtime/save
1687
+
1688
+ - parseSaveAutoMs (function): function parseSaveAutoMs(auto: string): number
1689
+ - saveScopeIncludesPlayer (function): function saveScopeIncludesPlayer(scope: SaveScope): boolean
1690
+ - saveScopeIncludesChunks (function): function saveScopeIncludesChunks(scope: SaveScope): boolean
1691
+ - isSaveEnabled (function): function isSaveEnabled(config: SaveConfig): config is Exclude<SaveConfig, "none">
1692
+ - SaveScope (type): type SaveScope = "player" | "chunks" | "player+chunks"
1693
+ - SaveConfig (type): type SaveConfig = | "none" | { auto: string; scope: SaveScope; }
1694
+
1695
+ ### @jgengine/core/runtime/snapshot
1696
+
1697
+ - createEmptyServerRow (function): function createEmptyServerRow(): RuntimeServerRow
1698
+ - createEmptyPlayerRow (function): function createEmptyPlayerRow(userId: string): RuntimePlayerRow
1699
+ - createRuntimeSnapshot (function): function createRuntimeSnapshot(args: { gameId: string; serverId: string; server?: RuntimeServerRow; players?: Record<string, RuntimePlayerRow>; chunks?: Record<string, RuntimeChunkRow>; }): GameRuntimeSnapshot
1700
+ - markServerDirty (function): function markServerDirty(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot
1701
+ - markPlayerDirty (function): function markPlayerDirty(snapshot: GameRuntimeSnapshot, userId: string): GameRuntimeSnapshot
1702
+ - clearDirtyFlags (function): function clearDirtyFlags(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot
1703
+ - splitProfilePlayer (function): function splitProfilePlayer(player: RuntimePlayerRow): { persistent: RuntimePlayerRow; session: Record<string, unknown>; }
1704
+ - RUNTIME_SNAPSHOT_VERSION (const): const RUNTIME_SNAPSHOT_VERSION: 1
1705
+ - RuntimeEntityRow (type): type RuntimeEntityRow = { instanceId: string; catalogId: string; position?: [number, number, number]; rotationY?: number; parentSpace?: string; group?: string; stats?: Record<string, { current: number; max: number; min?: number }>; targetInstanceId?: string | null; userId?: string; }
1706
+ - RuntimeObjectRow (type): type RuntimeObjectRow = { instanceId: string; catalogId: string; position: [number, number, number]; rotationY?: number; parentSpace?: string; flags?: Record<string, unknown>; }
1707
+ - RuntimeInventorySlot (type): type RuntimeInventorySlot = { item: string; count: number; slot?: number; }
1708
+ - RuntimePlayerRow (type): type RuntimePlayerRow = { userId: string; inventories: Record<string, RuntimeInventorySlot[]>; economy: Record<string, number>; unlocks: string[]; quests?: unknown; social?: unknown; leaderboard?: Record<string, number>; session?: Record<string, unknown>; }
1709
+ - RuntimeChunkRow (type): type RuntimeChunkRow = { chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string, unknown>; }
1710
+ - RuntimeServerRow (type): type RuntimeServerRow = { entities: RuntimeEntityRow[]; objects: RuntimeObjectRow[]; session: Record<string, unknown>; feeds?: Record<string, unknown[]>; }
1711
+ - RuntimeProfileRow (type): type RuntimeProfileRow = { userId: string; gameId: string; player: RuntimePlayerRow; updatedAt: number; }
1712
+ - GameRuntimeSnapshot (type): type GameRuntimeSnapshot = { version: number; gameId: string; serverId: string; server: RuntimeServerRow; players: Record<string, RuntimePlayerRow>; chunks: Record<string, RuntimeChunkRow>; revision: number; dirty: { server: boolean; players: string[]; chunks: string[]; }; }
1713
+
1714
+ ### @jgengine/core/runtime/transport
1715
+
1716
+ - JoinServerResult (type): type JoinServerResult = { serverId: string; isNew: boolean; }
1717
+ - RunCommandArgs (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; }
1718
+ - TransportRunCommandResult (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string }
1719
+ - GameRuntimeServerView (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown; updatedAt: number; }
1720
+ - GameRuntimePlayerView (type): type GameRuntimePlayerView = { userId: string; gameId: string; playerState: unknown; updatedAt: number; }
1721
+ - GameRuntimeFeedView (type): type GameRuntimeFeedView = { action: string; entries: unknown[]; }
1722
+ - GameRuntimeTransport (type): type GameRuntimeTransport = { joinServer: (args: { gameId: string; serverId?: string }) => Promise<JoinServerResult>; leaveServer: (args: { serverId: string }) => Promise<void>; runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>; }
1723
+ - FeedUnsubscribe (type): type FeedUnsubscribe = () => void
1724
+ - GameRuntimeFeeds (type): type GameRuntimeFeeds = { subscribeServer: ( serverId: string, onChange: (view: GameRuntimeServerView | null) => void, ) => FeedUnsubscribe; subscribePlayer: ( args: { serverId: string }, onChange: (view: GameRuntimePlayerView | null) => void, ) => FeedUnsubscribe; subscribeFeed: ( args: { serverId:…
1725
+ - GameBackend (type): type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = { transport: GameRuntimeTransport; feeds?: GameRuntimeFeeds; presence?: PresenceTransport<TPresenceRow, TPresenceLocation, TGameId>; }
1726
+ - PresencePoseRow (type): type PresencePoseRow = { userId: string; position: { x: number; y: number; z: number }; rotationY: number; rotationPitch: number; lastSeenAt: number; }
1727
+ - PresenceSync (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; }
1728
+ - LiveGameBackend (type): type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & { presenceSync: PresenceSync; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; chatSyncFor…
1729
+ - MultiplayerSession (type): type MultiplayerSession = { gameId: string; userId: string; backend: LiveGameBackend; feedActions: string[]; }
1730
+
1731
+ ### @jgengine/core/scene/assetCatalog
1732
+
1733
+ - createAssetCatalog (function): function createAssetCatalog<TMeta extends ModelAssetRef = ModelAssetRef>(): AssetCatalog<TMeta>
1734
+ - ModelDims (interface): interface ModelDims
1735
+ - ModelAssetRef (interface): interface ModelAssetRef
1736
+ - AssetCatalog (interface): interface AssetCatalog<TMeta extends ModelAssetRef = ModelAssetRef>
1737
+
1738
+ ### @jgengine/core/scene/assetPreload
1739
+
1740
+ - createSceneAssetPreloader (function): function createSceneAssetPreloader(fetcher: SceneAssetFetcher): { preloadUrl(url: string): void; preloadUrls(urls: readonly string[]): void; hasPreloaded(url: string): boolean; reset(): void; }
1741
+ - SceneAssetFetcher (type): type SceneAssetFetcher = (url: string) => void
1742
+
1743
+ ### @jgengine/core/scene/autoTarget
1744
+
1745
+ - selectAutoTarget (function): function selectAutoTarget(policy: AutoTargetPolicy, fromId: string, deps: AutoTargetDeps): string | null
1746
+ - createAutoTargeter (function): function createAutoTargeter(policy: AutoTargetPolicy, deps: AutoTargetDeps): AutoTargeter
1747
+ - AutoTargetPolicy (type): type AutoTargetPolicy = | "nearest" | "farthest" | "random" | "strongest" | "weakest" | "first" | "last"
1748
+ - AutoTargetDeps (interface): interface AutoTargetDeps
1749
+ - AutoTargeter (interface): interface AutoTargeter
1750
+
1751
+ ### @jgengine/core/scene/behaviors
1752
+
1753
+ - wander (function): function wander({ radius }: { radius: number }): WanderBehavior
1754
+ - patrol (function): function patrol({ waypoints, speed, loop = true, }: { waypoints: readonly Waypoint[]; speed: number; loop?: boolean; }): PatrolBehavior
1755
+ - promptable (function): function promptable(prompt: ProximityPrompt): PromptableBehavior
1756
+ - talkable (function): function talkable(dialogueId: string): PromptableBehavior
1757
+ - player (function): function player(): PlayerBehavior
1758
+ - WanderBehavior (interface): interface WanderBehavior
1759
+ - PatrolBehavior (interface): interface PatrolBehavior
1760
+ - PromptableBehavior (interface): interface PromptableBehavior
1761
+ - PlayerBehavior (interface): interface PlayerBehavior
1762
+ - BehaviorDescriptor (type): type BehaviorDescriptor = | WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior
1763
+
1764
+ ### @jgengine/core/scene/captureCheck
1765
+
1766
+ - captureChance (function): function captureChance(input: CaptureCheckInput): number
1767
+ - rollCapture (function): function rollCapture(input: CaptureCheckInput, rng: () => number = Math.random): boolean
1768
+ - CaptureCheckInput (interface): interface CaptureCheckInput
1769
+
1770
+ ### @jgengine/core/scene/entityStats
1771
+
1772
+ - getStatValue (function): function getStatValue(map: StatValueMap, statId: string): StatValue | null
1773
+ - setStatValue (function): function setStatValue(map: StatValueMap, statId: string, patch: StatValuePatch): StatValueMap
1774
+ - applyPoolDelta (function): function applyPoolDelta(map: StatValueMap, statId: string, amount: number): PoolDeltaResult
1775
+ - seedStatValues (function): function seedStatValues(catalogStats: StatCatalog): StatValueMap
1776
+ - createEntityStatsApi (function): function createEntityStatsApi(resolve: (instanceId: string) => StatValueMap | undefined): EntityStatsApi
1777
+ - StatValue (interface): interface StatValue
1778
+ - StatValuePatch (interface): interface StatValuePatch
1779
+ - StatValueMap (type): type StatValueMap = Record<string, StatValue>
1780
+ - StatCatalog (type): type StatCatalog = Record<string, { max: number; min?: number; current?: number }>
1781
+ - PoolDeltaResult (type): type PoolDeltaResult = | { status: "ok"; map: StatValueMap; stat: StatValue; hitMin: boolean; hitMax: boolean } | { status: "rejected"; reason: string }
1782
+ - EntityStatsApi (interface): interface EntityStatsApi
1783
+
1784
+ ### @jgengine/core/scene/entityStore
1785
+
1786
+ - groundSpeed (function): function groundSpeed(entity: SceneEntity<unknown>): number — Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code.
1787
+ - movedWhileFrozen (function): function movedWhileFrozen(entity: SceneEntity<unknown>, threshold = DEFAULT_FROZEN_MOVE_THRESHOLD): boolean
1788
+ - createEntityStore (function): function createEntityStore<TMeta = undefined>(): EntityStore<TMeta>
1789
+ - EntityRole (type): type EntityRole = "player" | "npc" | "prop"
1790
+ - EntityMovement (interface): interface EntityMovement
1791
+ - EntityPosition (type): type EntityPosition = readonly [number, number, number]
1792
+ - SpawnPositionInput (type): type SpawnPositionInput = EntityPosition | { x: number; y: number; z: number }
1793
+ - SceneEntity (interface): interface SceneEntity<TMeta = undefined>
1794
+ - SpawnOptions (interface): interface SpawnOptions<TMeta = undefined>
1795
+ - EntityPose (interface): interface EntityPose
1796
+ - SpawnPose (interface): interface SpawnPose
1797
+ - EntityStore (interface): interface EntityStore<TMeta = undefined>
1798
+
1799
+ ### @jgengine/core/scene/form
1800
+
1801
+ - createForms (function): function createForms(deps: FormsDeps): Forms
1802
+ - FormDef (interface): interface FormDef
1803
+ - FormChangedEvent (interface): interface FormChangedEvent
1804
+ - FormEntities (interface): interface FormEntities
1805
+ - FormTime (interface): interface FormTime
1806
+ - FormEvents (interface): interface FormEvents
1807
+ - FormsDeps (interface): interface FormsDeps
1808
+ - Forms (interface): interface Forms
1809
+
1810
+ ### @jgengine/core/scene/mount
1811
+
1812
+ - createMountController (function): function createMountController(): MountController
1813
+ - MountKind (type): type MountKind = "ground" | "boat" | "flying" | "swimming"
1814
+ - MountKit (interface): interface MountKit
1815
+ - MountSeat (interface): interface MountSeat
1816
+ - RideableConfig (interface): interface RideableConfig
1817
+ - SeatRef (interface): interface SeatRef
1818
+ - Occupant (interface): interface Occupant
1819
+ - MountResult (type): type MountResult = { ok: true; seat: MountSeat } | { ok: false; reason: string }
1820
+ - MountController (class): class MountController — Mount / rideable control-transfer (issue #83). Registers rideables (each with one or more seats — a control seat drives, the rest ride) and tracks who is on what. It owns no camera or physics: game code reads `cameraTarget(riderId)` to point the follow camera at the mount, and `driveTarget(riderId)` to route that rider's {@link import("../physics/vehicleBody").AxisInput}-driven input at the mount's movement kit — the same seam a horse, a truck, or a shared multi-seat ship all plug into.
1821
+
1822
+ ### @jgengine/core/scene/movementSpeed
1823
+
1824
+ - deriveWalkSpeed (function): function deriveWalkSpeed(base: number, modifiers: MovementSpeedModifiers): number — `base * (multiplier ?? 1) + (flatBonus ?? 0)`, clamped to never go negative.
1825
+ - applyStatDrivenSpeed (function): function applyStatDrivenSpeed(deps: MovementSpeedDeps, instanceId: string, config: MovementSpeedConfig): boolean — Recomputes `movement.walkSpeed` for an entity from its stats and writes it back via `deps.entities.update`. Either stat id is optional; an omitted or unresolved stat contributes its neutral value (multiplier 1, flat bonus 0) rather than failing. Returns false only when the entity itself doesn't exist.
1826
+ - MovementSpeedModifiers (interface): interface MovementSpeedModifiers
1827
+ - MovementSpeedDeps (interface): interface MovementSpeedDeps
1828
+ - MovementSpeedConfig (interface): interface MovementSpeedConfig
1829
+
1830
+ ### @jgengine/core/scene/objectQuery
1831
+
1832
+ - raycastObjects (function): function raycastObjects(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit | null
1833
+ - raycastObjectsAll (function): function raycastObjectsAll(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit[]
1834
+ - ObjectRaycastInput (interface): interface ObjectRaycastInput
1835
+ - ObjectRaycastHit (interface): interface ObjectRaycastHit
1836
+
1837
+ ### @jgengine/core/scene/objectStore
1838
+
1839
+ - createObjectStore (function): function createObjectStore(): ObjectStore
1840
+ - objectVisualScale (function): function objectVisualScale(visual: ObjectVisual | undefined): readonly [number, number, number]
1841
+ - ObjectVisual (interface): interface ObjectVisual
1842
+ - SceneObject (interface): interface SceneObject
1843
+ - PlaceOptions (interface): interface PlaceOptions
1844
+ - ObjectListFilter (interface): interface ObjectListFilter
1845
+ - ObjectStore (interface): interface ObjectStore
1846
+
1847
+ ### @jgengine/core/scene/paintLayer
1848
+
1849
+ - createPaintLayer (function): function createPaintLayer(): PaintLayer
1850
+ - PaintStroke (interface): interface PaintStroke
1851
+ - PaintLayer (interface): interface PaintLayer
1852
+
1853
+ ### @jgengine/core/scene/possession
1854
+
1855
+ - createPossession (function): function createPossession(deps: PossessionDeps): Possession
1856
+ - PossessionSwappedEvent (interface): interface PossessionSwappedEvent
1857
+ - PossessionEntities (interface): interface PossessionEntities
1858
+ - PossessionEvents (interface): interface PossessionEvents
1859
+ - PossessionDeps (interface): interface PossessionDeps
1860
+ - Possession (interface): interface Possession
1861
+
1862
+ ### @jgengine/core/scene/roster
1863
+
1864
+ - createRoster (function): function createRoster(deps: RosterDeps = {}): Roster
1865
+ - RosterEntry (interface): interface RosterEntry
1866
+ - RosterCaptureOptions (interface): interface RosterCaptureOptions
1867
+ - Roster (interface): interface Roster
1868
+ - RosterDeps (interface): interface RosterDeps
1869
+
1870
+ ### @jgengine/core/scene/selection
1871
+
1872
+ - screenRect (function): function screenRect(ax: number, ay: number, bx: number, by: number): ScreenRect — Normalize two drag corners (in any order) into a rectangle.
1873
+ - rectContainsPoint (function): function rectContainsPoint(rect: ScreenRect, x: number, y: number): boolean
1874
+ - selectWithinRect (function): function selectWithinRect(candidates: readonly ScreenPoint[], rect: ScreenRect): string[] — Ids of the projected candidates whose screen point falls inside the marquee.
1875
+ - isMarquee (function): function isMarquee(rect: ScreenRect, thresholdPx = 4): boolean — True when the drag is large enough to be a marquee rather than a click.
1876
+ - createSelectionSet (function): function createSelectionSet(initial?: Iterable<string>): SelectionSet — An ordered, deduplicated set of selected instance ids for RTS unit-command routing.
1877
+ - ScreenRect (interface): interface ScreenRect
1878
+ - ScreenPoint (interface): interface ScreenPoint
1879
+ - SelectionSet (interface): interface SelectionSet
1880
+
1881
+ ### @jgengine/core/scene/spatial
1882
+
1883
+ - distanceBetween (function): function distanceBetween(a: EntityPosition, b: EntityPosition): number
1884
+ - createSpatialApi (function): function createSpatialApi(options: SpatialApiOptions): SpatialApi
1885
+ - Aim (type): type Aim = | { origin: EntityPosition; direction: EntityPosition } | { yaw: number; pitch: number; spread?: number }
1886
+ - QueryArcOptions (interface): interface QueryArcOptions
1887
+ - MoveTowardOptions (interface): interface MoveTowardOptions
1888
+ - SpatialGridOptions (interface): interface SpatialGridOptions
1889
+ - SpatialApiOptions (interface): interface SpatialApiOptions
1890
+ - SpatialApi (interface): interface SpatialApi
1891
+
1892
+ ### @jgengine/core/scene/stationClaim
1893
+
1894
+ - createStationClaim (function): function createStationClaim(controller?: MountController): StationClaim
1895
+ - Station (interface): interface Station
1896
+ - SharedVehicleConfig (interface): interface SharedVehicleConfig
1897
+ - ClaimResult (type): type ClaimResult = | { ok: true; station: Station } | { ok: false; reason: "unknown_vehicle" | "unknown_facet" | "station_taken" | "vehicle_full" }
1898
+ - StationClaim (class): class StationClaim
1899
+
1900
+ ### @jgengine/core/scene/targeting
1901
+
1902
+ - createTargeting (function): function createTargeting(options: TargetingOptions): Targeting
1903
+ - TargetRelation (type): type TargetRelation = "hostile" | "friendly"
1904
+ - TargetFilter (type): type TargetFilter = TargetRelation | "any"
1905
+ - TargetingOptions (interface): interface TargetingOptions
1906
+ - CycleTargetOptions (interface): interface CycleTargetOptions
1907
+ - Targeting (interface): interface Targeting
1908
+
1909
+ ### @jgengine/core/sensor/concealment
1910
+
1911
+ - colorDistance (function): function colorDistance(a: ColorHex, b: ColorHex): number
1912
+ - concealmentScore (function): function concealmentScore(entityColors: readonly ColorHex[], backgroundColors: readonly ColorHex[]): number
1913
+ - createConcealmentSensor (function): function createConcealmentSensor(config?: { threshold?: number }): ConcealmentSensor
1914
+ - ColorHex (type): type ColorHex = string
1915
+ - ConcealmentTarget (interface): interface ConcealmentTarget
1916
+ - ConcealmentSample (interface): interface ConcealmentSample
1917
+ - ConcealmentSensor (interface): interface ConcealmentSensor
1918
+
1919
+ ### @jgengine/core/sensor/freezeMonitor
1920
+
1921
+ - createFreezeMonitor (function): function createFreezeMonitor(config?: { toleranceSpeed?: number; graceSeconds?: number }): FreezeMonitor
1922
+ - FreezeSubject (interface): interface FreezeSubject
1923
+ - FreezeViolation (interface): interface FreezeViolation
1924
+ - FreezeMonitor (interface): interface FreezeMonitor
1925
+
1926
+ ### @jgengine/core/sensor/frustumSensor
1927
+
1928
+ - projectToView (function): function projectToView(camera: FrustumCamera, point: EntityPosition): FrustumProjection — Project a world point into the camera's view. Pure geometry — no three.js — so a game can run "is this framed?" checks headlessly (photo-mode scoring, server-side kill-cam validation) as well as from the live render camera.
1929
+ - framingScore (function): function framingScore(projection: FrustumProjection, config?: FramingConfig): number — 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection.
1930
+ - createFrustumSensor (function): function createFrustumSensor(config?: FramingConfig): FrustumSensor — A view-frustum sensor on a held camera object: which entities are in frame, how well framed they are, and how long they've stayed on-screen (photo-mode "is this subject framed", Content Warning-style monster-filming scoring).
1931
+ - FrustumCamera (interface): interface FrustumCamera
1932
+ - FrustumTarget (interface): interface FrustumTarget
1933
+ - FrustumProjection (interface): interface FrustumProjection
1934
+ - FramingConfig (interface): interface FramingConfig
1935
+ - FrustumSample (interface): interface FrustumSample
1936
+ - FrustumSensor (interface): interface FrustumSensor
1937
+
1938
+ ### @jgengine/core/sensor/hiddenStateProbe
1939
+
1940
+ - probeHiddenState (function): function probeHiddenState(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading | null — A sensor verb: probe a hidden zone/entity state variable in range and surface a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike `proximityPrompt` (a UI affordance), this reads world state the player has no other way to see — the strongest in-range reading wins, mirroring how a real handheld sensor needle settles on the loudest nearby source.
1941
+ - probeHiddenStateAll (function): function probeHiddenStateAll(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading[] — All in-range readings for `variableId`, strongest first.
1942
+ - HiddenStateValue (type): type HiddenStateValue = number | string | boolean
1943
+ - HiddenStateSource (interface): interface HiddenStateSource
1944
+ - SensorProbeOptions (interface): interface SensorProbeOptions
1945
+ - SensorReading (interface): interface SensorReading
1946
+
1947
+ ### @jgengine/core/sensor/recordingBuffer
1948
+
1949
+ - createRecordingBuffer (function): function createRecordingBuffer<T>(options: RecordingBufferOptions = {}): RecordingBuffer<T> — A session-recording buffer for replay / photo mode / kill-cam: append timestamped snapshots (poses, camera state, whatever `T` is) on game-time, then seek/scrub the recording independent of the live sim. Frames are expected in non-decreasing `t` order (one push per tick); `seek`/`range` binary-search on that assumption.
1950
+ - RecordingFrame (interface): interface RecordingFrame<T>
1951
+ - RecordingBufferOptions (interface): interface RecordingBufferOptions
1952
+ - RecordingBuffer (interface): interface RecordingBuffer<T>
1953
+
1954
+ ### @jgengine/core/sensor/revealQuery
1955
+
1956
+ - createRevealQuery (function): function createRevealQuery(options: RevealQueryOptions): RevealQuery — `inRadius` already ignores occlusion; combat's AoE resolution is what layers a line-of-sight filter on top of it (see `effect({ at, radius, los })`). This is the same unfiltered radius query, scoped to catalog-declared tags and shaped for a vision readout (Dark Sight / detective-vision reveal) instead of damage resolution — it never consults an occluder, by design.
1957
+ - RevealHit (interface): interface RevealHit
1958
+ - RevealQueryOptions (interface): interface RevealQueryOptions
1959
+ - RevealQuery (interface): interface RevealQuery
1960
+
1961
+ ### @jgengine/core/session/contestedChannel
1962
+
1963
+ - createContestedChannel (function): function createContestedChannel(config: ContestedChannelConfig): ContestedChannel
1964
+ - ContestedPhase (type): type ContestedPhase = "idle" | "active" | "paused" | "contested" | "complete" | "interrupted"
1965
+ - ContestReaction (type): type ContestReaction = "pause" | "decay"
1966
+ - ContestedChannelConfig (interface): interface ContestedChannelConfig
1967
+ - ContestedEventKind (type): type ContestedEventKind = "start" | "tick" | "contested" | "paused" | "complete" | "interrupted"
1968
+ - ContestedEvent (interface): interface ContestedEvent
1969
+ - ContestedSnapshot (interface): interface ContestedSnapshot
1970
+ - ContestedChannel (interface): interface ContestedChannel
1971
+
1972
+ ### @jgengine/core/session/extraction
1973
+
1974
+ - createRaidSession (function): function createRaidSession(config: RaidSessionConfig): RaidSession
1975
+ - ExtractPoint (interface): interface ExtractPoint
1976
+ - RaidSessionConfig (interface): interface RaidSessionConfig
1977
+ - RaidStatus (type): type RaidStatus = "in-raid" | "extracting" | "extracted" | "dead"
1978
+ - ExtractionAttempt (interface): interface ExtractionAttempt
1979
+ - ExtractionResult (interface): interface ExtractionResult
1980
+ - DeathResult (interface): interface DeathResult
1981
+ - RaidPlayerSnapshot (interface): interface RaidPlayerSnapshot
1982
+ - RaidSession (interface): interface RaidSession
1983
+
1984
+ ### @jgengine/core/session/ring
1985
+
1986
+ - ringSampleAt (function): function ringSampleAt(config: RingConfig, time: number): RingSample
1987
+ - createRing (function): function createRing(config: RingConfig): Ring
1988
+ - RingPoint (type): type RingPoint = [number, number]
1989
+ - RingPhase (interface): interface RingPhase
1990
+ - RingConfig (interface): interface RingConfig
1991
+ - RingSample (interface): interface RingSample
1992
+ - RingHit (interface): interface RingHit
1993
+ - Ring (interface): interface Ring
1994
+
1995
+ ### @jgengine/core/session/roles
1996
+
1997
+ - assignRoles (function): function assignRoles(userIds: readonly string[], roles: readonly RoleSpec[], rng: () => number = Math.random): Record<string, string>
1998
+ - RoleSpec (interface): interface RoleSpec
1999
+
2000
+ ### @jgengine/core/session/roundState
2001
+
2002
+ - lossBonusFor (function): function lossBonusFor(rule: LossBonusRule | undefined, streak: number): number
2003
+ - createRoundState (function): function createRoundState(config: RoundConfig<RoundPhase>): RoundState<RoundPhase>
2004
+ - RoundPhase (type): type RoundPhase = string
2005
+ - RoundPhaseDurations (interface): interface RoundPhaseDurations — Default phase-duration shape for the built-in buy/live/end cycle; pass a wider `Record<string, number>` when using a custom `phaseOrder`.
2006
+ - LossBonusRule (interface): interface LossBonusRule
2007
+ - RoundTeam (interface): interface RoundTeam — A team entry with an optional role tag (e.g. "attacker", "defender") retrievable via `RoundState.roleOf`.
2008
+ - RoundConfig (interface): interface RoundConfig<TPhase extends string = RoundPhase>
2009
+ - RoundEventKind (type): type RoundEventKind = | "phase.start" | "phase.end" | "round.win" | "round.economy" | "match.end"
2010
+ - RoundEconomy (interface): interface RoundEconomy
2011
+ - RoundEvent (interface): interface RoundEvent<TPhase extends string = RoundPhase>
2012
+ - PhaseEndHook (type): type PhaseEndHook<TPhase extends string = RoundPhase> = ( endingPhase: TPhase, nextPhase: TPhase, round: number, ) => void
2013
+ - RoundSnapshot (interface): interface RoundSnapshot<TPhase extends string = RoundPhase>
2014
+ - RoundState (interface): interface RoundState<TPhase extends string = RoundPhase>
2015
+
2016
+ ### @jgengine/core/stats/accumulatorMeter
2017
+
2018
+ - tierAt (function): function tierAt(value: number, tiers: readonly MeterTier[]): string | null
2019
+ - createAccumulatorMeter (function): function createAccumulatorMeter(config: AccumulatorMeterConfig): AccumulatorMeter
2020
+ - MeterTier (interface): interface MeterTier
2021
+ - MeterMode (type): type MeterMode = "hold" | "reset"
2022
+ - AccumulatorMeterConfig (interface): interface AccumulatorMeterConfig
2023
+ - MeterAddResult (interface): interface MeterAddResult
2024
+ - AccumulatorMeter (interface): interface AccumulatorMeter
2025
+
2026
+ ### @jgengine/core/stats/eventMeter
2027
+
2028
+ - createEventMeter (function): function createEventMeter(config: EventMeterConfig): EventMeter
2029
+ - EventMeterConfig (interface): interface EventMeterConfig
2030
+ - EventMeterFeedResult (interface): interface EventMeterFeedResult
2031
+ - EventMeter (interface): interface EventMeter
2032
+
2033
+ ### @jgengine/core/stats/rollCheck
2034
+
2035
+ - rollCheck (function): function rollCheck(input: CheckInput, rng: () => number = Math.random): CheckResult
2036
+ - CheckAdvantage (type): type CheckAdvantage = "advantage" | "disadvantage" | "normal"
2037
+ - CheckInput (interface): interface CheckInput
2038
+ - CheckResult (interface): interface CheckResult
2039
+
2040
+ ### @jgengine/core/stats/statModifiers
2041
+
2042
+ - createStats (function): function createStats<TStat extends string>(base: Record<TStat, number>): Stats<TStat>
2043
+ - StatModifier (interface): interface StatModifier
2044
+ - StatModifierSet (type): type StatModifierSet<TStat extends string> = Partial<Record<TStat, StatModifier>>
2045
+ - Stats (interface): interface Stats<TStat extends string>
2046
+
2047
+ ### @jgengine/core/store/changeSignal
2048
+
2049
+ - createChangeSignal (function): function createChangeSignal(): ChangeSignal
2050
+ - notifyAfter (function): function notifyAfter<T extends object, K extends keyof T>(target: T, methods: readonly K[], notify: () => void): T
2051
+ - ChangeSignal (interface): interface ChangeSignal
2052
+
2053
+ ### @jgengine/core/store/engineStore
2054
+
2055
+ - EngineStoreConfig (interface): interface EngineStoreConfig<TState, TAction, TEventMap extends object>
2056
+ - EngineStore (class): class EngineStore<TState, TAction, TEventMap extends object>
2057
+
2058
+ ### @jgengine/core/store/features
2059
+
2060
+ - composeStateFeatures (function): function composeStateFeatures<TState, TAction, TEventMap extends object>(features: readonly StateFeature<TState, TAction, TEventMap>[]): { reduce(state: TState, action: TAction): TState; derive(state: TState): TState; onTransition(previous: TState, next: TState, emit: <K extends keyof TEventMap>(eve…
2061
+ - StateFeature (interface): interface StateFeature<TState, TAction, TEventMap extends object>
2062
+
2063
+ ### @jgengine/core/store/observableKeyedStore
2064
+
2065
+ - createObservableKeyedStore (function): function createObservableKeyedStore<T>(areEqual?: (previous: T, next: T) => boolean): ObservableKeyedStore<T>
2066
+ - ObservableKeyedStore (interface): interface ObservableKeyedStore<T>
2067
+
2068
+ ### @jgengine/core/survival/decayMeter
2069
+
2070
+ - createDecayMeterSet (function): function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet — Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains (or recovers) on game-time `dt` at a configurable rate, refills from consumables or actions, and raises moodle statuses at thresholds. Rate modifiers let the environment drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads an environment field then calls `setRateModifier`.
2071
+ - MeterThreshold (interface): interface MeterThreshold
2072
+ - DecayMeterConfig (interface): interface DecayMeterConfig
2073
+ - DecayMeterState (interface): interface DecayMeterState
2074
+ - DecayMeterSet (interface): interface DecayMeterSet
2075
+
2076
+ ### @jgengine/core/survival/moodle
2077
+
2078
+ - stackMoodles (function): function stackMoodles(...groups: readonly (readonly Moodle[])[]): Moodle[] — Merge any number of moodle groups into one stack — meters, ailments, and buffs share this display. Same-id moodles fold together (stacks add, worst severity wins); the result is ordered worst-first so the HUD reads critical statuses at a glance.
2079
+ - createMoodleStack (function): function createMoodleStack(): MoodleStack — A stateful holder for timed status moodles (food buffs, temporary shelter, warmth). Meters and multi-region health derive their own moodles on read; combine all three through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display.
2080
+ - MoodleSeverity (type): type MoodleSeverity = "good" | "neutral" | "warning" | "critical"
2081
+ - MoodleSource (type): type MoodleSource = "meter" | "ailment" | "buff"
2082
+ - Moodle (interface): interface Moodle
2083
+ - MOODLE_SEVERITY_ORDER (const): const MOODLE_SEVERITY_ORDER: Record<MoodleSeverity, number>
2084
+ - TimedMoodleInput (interface): interface TimedMoodleInput
2085
+ - MoodleStack (interface): interface MoodleStack
2086
+
2087
+ ### @jgengine/core/survival/regionHealth
2088
+
2089
+ - createMultiRegionHealth (function): function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth
2090
+ - HealthRegionConfig (interface): interface HealthRegionConfig
2091
+ - AilmentConfig (interface): interface AilmentConfig
2092
+ - MultiRegionHealthConfig (interface): interface MultiRegionHealthConfig
2093
+ - RegionHealthState (interface): interface RegionHealthState
2094
+ - AilmentInstance (interface): interface AilmentInstance
2095
+ - DamageResult (interface): interface DamageResult
2096
+ - TreatResult (interface): interface TreatResult
2097
+ - MultiRegionHealth (interface): interface MultiRegionHealth
2098
+
2099
+ ### @jgengine/core/tactics/fallingGrid
2100
+
2101
+ - gravityIntervalMs (function): function gravityIntervalMs(level: number, config: GravityIntervalConfig = {}): number — Gravity tick interval for a given level: linear speed-up with a floor, generalizing `Games/block-stacker`'s `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds with configurable base/step/floor.
2102
+ - createFallingGrid (function): function createFallingGrid<TCell>(config: FallingGridConfig): FallingGrid<TCell> — A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy, footprint placement/collision, line-clear collapse, column-gravity settle (for match-3/sand style cascades), and a pure lock-delay helper. Extracted from `Games/block-stacker`'s tetris logic so any falling-block game can adopt it instead of reimplementing the grid.
2103
+ - FallingGridCell (type): type FallingGridCell = readonly [number, number]
2104
+ - FallingGridConfig (interface): interface FallingGridConfig
2105
+ - LockState (interface): interface LockState — Elapsed grounded time for the lock-delay state machine. `null` means airborne / not timing.
2106
+ - FallingGridSnapshot (interface): interface FallingGridSnapshot<TCell>
2107
+ - FallingGrid (interface): interface FallingGrid<TCell>
2108
+ - GravityIntervalConfig (interface): interface GravityIntervalConfig
2109
+
2110
+ ### @jgengine/core/tactics/predictiveQuery
2111
+
2112
+ - predictAreaEffect (function): function predictAreaEffect(deps: PredictiveDeps, input: AreaPredictInput): PredictedTarget[]
2113
+ - predictArcEffect (function): function predictArcEffect(deps: PredictiveDeps, input: ArcPredictInput): PredictedTarget[]
2114
+ - predictTiles (function): function predictTiles(input: TilePredictInput): Tile[]
2115
+ - PredictiveDeps (interface): interface PredictiveDeps
2116
+ - PredictedTarget (interface): interface PredictedTarget
2117
+ - AreaPredictInput (interface): interface AreaPredictInput extends AreaTargetInput
2118
+ - ArcPredictInput (interface): interface ArcPredictInput
2119
+ - TilePredictInput (interface): interface TilePredictInput
2120
+
2121
+ ### @jgengine/core/tactics/snapshot
2122
+
2123
+ - deepClone (function): function deepClone<T>(value: T): T
2124
+ - createSnapshotStore (function): function createSnapshotStore(): SnapshotStore
2125
+ - SnapshotSlice (interface): interface SnapshotSlice<T = unknown>
2126
+ - Snapshot (type): type Snapshot = Record<string, unknown>
2127
+ - SnapshotStore (interface): interface SnapshotStore
2128
+
2129
+ ### @jgengine/core/tactics/surface
2130
+
2131
+ - createSurfaceLayer (function): function createSurfaceLayer(config: SurfaceLayerConfig): SurfaceLayer
2132
+ - SurfaceKindDef (interface): interface SurfaceKindDef
2133
+ - SurfaceReaction (interface): interface SurfaceReaction
2134
+ - SurfaceLayerConfig (interface): interface SurfaceLayerConfig
2135
+ - SurfacePatch (interface): interface SurfacePatch
2136
+ - SurfaceEvent (type): type SurfaceEvent = | { kind: "apply"; tile: Tile; surface: string } | { kind: "react"; tile: Tile; consumed: readonly [string, string]; produced: string | null } | { kind: "expire"; tile: Tile; surface: string }
2137
+ - SurfaceCellKind (interface): interface SurfaceCellKind
2138
+ - SurfaceCell (interface): interface SurfaceCell
2139
+ - SurfaceLayerSnapshot (type): type SurfaceLayerSnapshot = Record<string, Record<string, number | null>>
2140
+ - SurfaceLayer (interface): interface SurfaceLayer
2141
+
2142
+ ### @jgengine/core/tactics/tacticalGrid
2143
+
2144
+ - createTacticalGrid (function): function createTacticalGrid(config: TacticalGridConfig): TacticalGrid
2145
+ - Tile (type): type Tile = readonly [number, number]
2146
+ - TacticalGridWorldConfig (interface): interface TacticalGridWorldConfig — Maps abstract `[col, row]` tiles onto world-space `[x, z]`, mirroring `navGrid`'s bounds+cellSize convention.
2147
+ - TacticalGridConfig (interface): interface TacticalGridConfig
2148
+ - ReachableOptions (interface): interface ReachableOptions
2149
+ - ReachableTile (interface): interface ReachableTile
2150
+ - PushObstacle (type): type PushObstacle = "wall" | "edge"
2151
+ - PushCollision (interface): interface PushCollision
2152
+ - PushMove (interface): interface PushMove
2153
+ - PushResult (interface): interface PushResult
2154
+ - PushOptions (interface): interface PushOptions
2155
+ - TacticalGridSnapshot (interface): interface TacticalGridSnapshot
2156
+ - TacticalGrid (interface): interface TacticalGrid
2157
+
2158
+ ### @jgengine/core/time/beatClock
2159
+
2160
+ - createBeatClock (function): function createBeatClock(config: BeatClockConfig, onBeat?: (beatIndex: number) => void): BeatClock
2161
+ - nextBeatTime (function): function nextBeatTime(nowSec: number, beatDurationSec: number, epsilon = QUANTIZE_EPSILON): number — The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one.
2162
+ - createBeatInputBuffer (function): function createBeatInputBuffer<T>(beatDurationSec: number): BeatInputBuffer<T>
2163
+ - BeatClockConfig (interface): interface BeatClockConfig
2164
+ - BeatSnapshot (interface): interface BeatSnapshot
2165
+ - BeatClock (interface): interface BeatClock
2166
+ - BufferedAction (interface): interface BufferedAction<T>
2167
+ - BeatInputBuffer (interface): interface BeatInputBuffer<T>
2168
+
2169
+ ### @jgengine/core/time/calendarClock
2170
+
2171
+ - computeGameTime (function): function computeGameTime(gameTimestamp: number): GameTime
2172
+ - computeClientServerOffset (function): function computeClientServerOffset(serverNow: number, clientNow: number): number
2173
+ - getGameClockTimestamp (function): function getGameClockTimestamp(createdAt: number, serverNow: number, clientNow: number, gameTimeScale?: number): number
2174
+ - GameTime (interface): interface GameTime
2175
+
2176
+ ### @jgengine/core/time/gameClock
2177
+
2178
+ - sanitizeGameTimeScale (function): function sanitizeGameTimeScale(timeScale?: number | null): number
2179
+ - getScaledElapsedMs (function): function getScaledElapsedMs(createdAt: number, now: number, timeScale?: number | null): number
2180
+ - getCurrentGameTimestamp (function): function getCurrentGameTimestamp(createdAt: number, now: number, timeScale?: number | null): number
2181
+ - computeGameDay (function): function computeGameDay(createdAt: number, now: number = Date.now(), timeScale?: number | null): number
2182
+ - DEFAULT_TIME_SCALE (const): const DEFAULT_TIME_SCALE: 24 — Simulation clock: real time scaled into game time, plus the derived game-day counter.
2183
+ - SECONDS_PER_GAME_DAY (const): const SECONDS_PER_GAME_DAY: number
2184
+
2185
+ ### @jgengine/core/time/serverTick
2186
+
2187
+ - planServerTick (function): function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number): ServerTickPlan<TSystemId> — Decides which systems a heartbeat should run. A system with no anchor runs immediately. Returned anchors carry only systems present in the pipeline, so removed systems clean up.
2188
+ - TickSystemDefinition (interface): interface TickSystemDefinition<TSystemId extends string = string>
2189
+ - TickAnchors (type): type TickAnchors = Record<string, number> — Last-run timestamp per system id.
2190
+ - ServerTickPlan (interface): interface ServerTickPlan<TSystemId extends string = string>
2191
+
2192
+ ### @jgengine/core/time/simClock
2193
+
2194
+ - createSimClock (function): function createSimClock(options: SimClockOptions = {}): SimClock
2195
+ - TimeConfig (interface): interface TimeConfig
2196
+ - CalendarTime (interface): interface CalendarTime
2197
+ - ClockSnapshot (interface): interface ClockSnapshot
2198
+ - SimClock (interface): interface SimClock
2199
+ - SimClockOptions (interface): interface SimClockOptions
2200
+
2201
+ ### @jgengine/core/turn/commit
2202
+
2203
+ - createCommitController (function): function createCommitController<TAction>(config: CommitControllerConfig): CommitController<TAction>
2204
+ - CommitMode (type): type CommitMode = "immediate" | "simultaneous" | "rewind"
2205
+ - SubmittedAction (interface): interface SubmittedAction<TAction>
2206
+ - CommitOutcome (interface): interface CommitOutcome<TAction>
2207
+ - CommitControllerConfig (interface): interface CommitControllerConfig
2208
+ - CommitController (interface): interface CommitController<TAction>
2209
+
2210
+ ### @jgengine/core/turn/intent
2211
+
2212
+ - createIntentBoard (function): function createIntentBoard<TKind extends string = string>(): IntentBoard<TKind>
2213
+ - DeclaredIntent (interface): interface DeclaredIntent<TKind extends string = string>
2214
+ - IntentBoard (interface): interface IntentBoard<TKind extends string = string>
2215
+
2216
+ ### @jgengine/core/turn/turnLoop
2217
+
2218
+ - createTurnLoop (function): function createTurnLoop<TAction = unknown>(config: TurnLoopConfig): TurnLoop<TAction>
2219
+ - CommitMode (type): type CommitMode = "immediate" | "simultaneous" | "rewind"
2220
+ - CommitController (interface): interface CommitController<TAction>
2221
+ - PoolConfig (interface): interface PoolConfig
2222
+ - TurnLoopConfig (interface): interface TurnLoopConfig
2223
+ - PoolState (interface): interface PoolState
2224
+ - TurnState (interface): interface TurnState
2225
+ - TurnLoopSnapshot (interface): interface TurnLoopSnapshot
2226
+ - TurnLoop (interface): interface TurnLoop<TAction = unknown>
2227
+
2228
+ ### @jgengine/core/world/buildingIndex
2229
+
2230
+ - buildingIndex (function): function buildingIndex(buildings: readonly GeneratedBuilding[]): BuildingIndex
2231
+ - BuildingHit (interface): interface BuildingHit
2232
+ - BuildingIndex (interface): interface BuildingIndex
2233
+
2234
+ ### @jgengine/core/world/buildings
2235
+
2236
+ - createBuildingConfig (function): function createBuildingConfig(input: BuildingConfigInput = {}): BuildingConfig
2237
+ - generateBuilding (function): function generateBuilding(input: BuildingConfigInput = {}): GeneratedBuilding
2238
+ - createBuildingGrid (function): function createBuildingGrid(config: BuildingGridConfig): BuildingLot[]
2239
+ - generateBuildingDistrict (function): function generateBuildingDistrict(config: BuildingGridConfig): GeneratedBuilding[]
2240
+ - BuildingSeed (type): type BuildingSeed = number | string
2241
+ - Vec3 (type): type Vec3 = readonly [number, number, number]
2242
+ - BuildingFacade (type): type BuildingFacade = "front" | "back" | "left" | "right" | "roof"
2243
+ - BuildingPartKind (type): type BuildingPartKind = | "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner"
2244
+ - BuildingPartTag (type): type BuildingPartTag = "ground" | "upper" | "open" | "closed" | "decor" | "structure"
2245
+ - BuildingProbabilities (interface): interface BuildingProbabilities
2246
+ - BuildingVariantCounts (interface): interface BuildingVariantCounts
2247
+ - BuildingConfig (interface): interface BuildingConfig
2248
+ - BuildingConfigInput (type): type BuildingConfigInput = Partial< Omit<BuildingConfig, "probabilities" | "variants"> > & { probabilities?: Partial<BuildingProbabilities>; variants?: Partial<BuildingVariantCounts>; }
2249
+ - BuildingKitSlot (interface): interface BuildingKitSlot
2250
+ - BuildingCellRef (interface): interface BuildingCellRef
2251
+ - BuildingPartPlacement (interface): interface BuildingPartPlacement
2252
+ - GeneratedBuilding (interface): interface GeneratedBuilding
2253
+ - BuildingLot (interface): interface BuildingLot
2254
+ - BuildingGridConfig (interface): interface BuildingGridConfig
2255
+ - DEFAULT_BUILDING_CONFIG (const): const DEFAULT_BUILDING_CONFIG: BuildingConfig
2256
+
2257
+ ### @jgengine/core/world/buildPermissions
2258
+
2259
+ - createPlotPermissions (function): function createPlotPermissions(config: PlotPermissionConfig): PlotPermissions
2260
+ - createContributionPool (function): function createContributionPool(goal: ContributionGoal): ContributionPool
2261
+ - BuildRole (type): type BuildRole = "owner" | "editor" | "viewer"
2262
+ - PlotPermissionConfig (interface): interface PlotPermissionConfig
2263
+ - PlotPermissionSnapshot (interface): interface PlotPermissionSnapshot
2264
+ - BuildActor (interface): interface BuildActor
2265
+ - PlotPermissions (interface): interface PlotPermissions
2266
+ - ContributionGoal (type): type ContributionGoal = Readonly<Record<string, number>>
2267
+ - ContributionResult (interface): interface ContributionResult
2268
+ - ContributionPool (interface): interface ContributionPool
2269
+
2270
+ ### @jgengine/core/world/carve
2271
+
2272
+ - carvableTerrain (function): function carvableTerrain(base: TerrainField): CarvableField
2273
+ - EMPTY_VOXEL (const): const EMPTY_VOXEL: 0
2274
+ - VoxelMaterial (interface): interface VoxelMaterial
2275
+ - VoxelMaterialTable (type): type VoxelMaterialTable = Readonly<Record<number, VoxelMaterial>>
2276
+ - VoxelVolumeConfig (interface): interface VoxelVolumeConfig
2277
+ - CarveOp (interface): interface CarveOp
2278
+ - DepositOp (interface): interface DepositOp
2279
+ - VoxelVolume (class): class VoxelVolume — A runtime-editable dense voxel grid — the carve/deposit op behind destructible dig worlds (Deep Rock Galactic tunnels, Astroneer terrain). Cells hold a material id (0 = empty); `carve` clears a sphere of solid cells that a tool is strong enough to break and returns how many it removed (feed that to a loot roll), `deposit` fills a sphere with a material. World↔cell mapping is `origin`+`scale`.
2280
+ - CraterOp (interface): interface CraterOp
2281
+ - MoundOp (interface): interface MoundOp
2282
+ - CarvableField (class): class CarvableField implements TerrainField — A `TerrainField` you can write craters and mounds into at runtime — the height-field side of destructible terrain (Helldivers 2 explosion craters, engineer-deposited berms). Wraps a base field and layers smooth radial deformations on top, so `sampleHeight` (and therefore ground-snap, collision, and the shell's terrain mesh) all read the deformed surface. `carve` digs a bowl, `deposit` raises a mound.
2283
+
2284
+ ### @jgengine/core/world/connectors
2285
+
2286
+ - socketsCompatible (function): function socketsCompatible(a: ConnectorSocket, b: ConnectorSocket): boolean
2287
+ - socketWorldPosition (function): function socketWorldPosition(socket: ConnectorSocket, origin: ConnectorVec3, rotationY: number): ConnectorVec3
2288
+ - worldSockets (function): function worldSockets(def: ConnectorPieceDef, piece: PlacedPiece): WorldSocket[]
2289
+ - collectWorldSockets (function): function collectWorldSockets(registry: ConnectorRegistry, pieces: readonly PlacedPiece[]): WorldSocket[]
2290
+ - snapToNearest (function): function snapToNearest(registry: ConnectorRegistry, placed: readonly PlacedPiece[], movingDef: ConnectorPieceDef, cursor: ConnectorVec3, options: SnapOptions = {}): SnapResult | null
2291
+ - ConnectorVec3 (type): type ConnectorVec3 = readonly [number, number, number]
2292
+ - ConnectorSocket (interface): interface ConnectorSocket
2293
+ - ConnectorPieceDef (interface): interface ConnectorPieceDef
2294
+ - PlacedPiece (interface): interface PlacedPiece
2295
+ - WorldSocket (interface): interface WorldSocket
2296
+ - SnapResult (interface): interface SnapResult
2297
+ - SnapOptions (interface): interface SnapOptions
2298
+ - ConnectorRegistry (type): type ConnectorRegistry = (kind: string) => ConnectorPieceDef | null
2299
+
2300
+ ### @jgengine/core/world/envField
2301
+
2302
+ - createEnvironmentField (function): function createEnvironmentField(config: EnvironmentFieldConfig = {}): EnvironmentField — A sampleable environment field: read temperature, wetness, sun/sky exposure, and ambient light at any world position and time. Built on the same renderer-free footing as terrain/wind/water so meters, spawn gating, and damage-in-sunlight read the world the shell renders — no three.js. Instantaneous and pure (no accumulation); stateful build-up belongs to a decay meter reading this field.
2303
+ - OccluderRect (interface): interface OccluderRect — A sky-occluding footprint — a roof, dense canopy, a cave mouth. `shade` 0..1 = fraction of sky blocked.
2304
+ - HeatSource (interface): interface HeatSource — A localized warmth source — campfire, forge, geothermal vent.
2305
+ - ShadeProvider (type): type ShadeProvider = readonly OccluderRect[] | ((x: number, z: number) => number)
2306
+ - ScalarField (type): type ScalarField = number | ((x: number, z: number, time: number) => number)
2307
+ - EnvironmentFieldConfig (interface): interface EnvironmentFieldConfig
2308
+ - EnvironmentSample (interface): interface EnvironmentSample
2309
+ - EnvironmentField (interface): interface EnvironmentField
2310
+
2311
+ ### @jgengine/core/world/environmentSummary
2312
+
2313
+ - resolveStructureBuildings (function): function resolveStructureBuildings(descriptor: BuildingEnvironmentDescriptor): GeneratedBuilding[]
2314
+ - summarizeEnvironment (function): function summarizeEnvironment(feature: EnvironmentWorldFeature): EnvironmentSummary
2315
+ - TerrainHeightStats (interface): interface TerrainHeightStats
2316
+ - TerrainSummary (interface): interface TerrainSummary
2317
+ - StructureSummary (interface): interface StructureSummary
2318
+ - VegetationSummary (interface): interface VegetationSummary
2319
+ - WaterSummary (interface): interface WaterSummary
2320
+ - WeatherSummary (interface): interface WeatherSummary
2321
+ - EnvironmentCounts (interface): interface EnvironmentCounts
2322
+ - EnvironmentSummary (interface): interface EnvironmentSummary
2323
+
2324
+ ### @jgengine/core/world/features
2325
+
2326
+ - padFlattenMasks (function): function padFlattenMasks(pads: readonly PadEnvironmentDescriptor[]): readonly TerrainFlattenMask[] — Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it.
2327
+ - environment (function): function environment(config: EnvironmentWorldConfig = {}): EnvironmentWorldFeature
2328
+ - terrain (function): function terrain(config: TerrainEnvironmentConfig = {}): TerrainEnvironmentDescriptor
2329
+ - sky (function): function sky(config: SkyEnvironmentConfig = {}): SkyEnvironmentDescriptor
2330
+ - rain (function): function rain(config: RainEnvironmentConfig = {}): RainEnvironmentDescriptor
2331
+ - snow (function): function snow(config: SnowEnvironmentConfig = {}): SnowEnvironmentDescriptor
2332
+ - grass (function): function grass(config: GrassEnvironmentConfig = {}): GrassEnvironmentDescriptor
2333
+ - ocean (function): function ocean(config: OceanEnvironmentConfig = {}): OceanEnvironmentDescriptor
2334
+ - building (function): function building(config: BuildingEnvironmentConfig = {}): BuildingEnvironmentDescriptor
2335
+ - pad (function): function pad(config: PadEnvironmentConfig): PadEnvironmentDescriptor
2336
+ - biomes (function): function biomes(config: BiomesWorldConfig): WorldFeature
2337
+ - voxel (function): function voxel(config: VoxelWorldConfig): WorldFeature
2338
+ - plots (function): function plots(config: PlotsWorldConfig = {}): WorldFeature
2339
+ - tilemap (function): function tilemap(config: TilemapWorldConfig): WorldFeature
2340
+ - flat (function): function flat(): WorldFeature
2341
+ - WorldBounds (interface): interface WorldBounds
2342
+ - EnvironmentVec2 (type): type EnvironmentVec2 = readonly [number, number]
2343
+ - EnvironmentArea (interface): interface EnvironmentArea extends WorldBounds
2344
+ - TerrainColors (interface): interface TerrainColors
2345
+ - TerrainFlattenMask (interface): interface TerrainFlattenMask
2346
+ - TerrainEnvironmentConfig (interface): interface TerrainEnvironmentConfig
2347
+ - RainEnvironmentConfig (interface): interface RainEnvironmentConfig
2348
+ - SnowEnvironmentConfig (interface): interface SnowEnvironmentConfig
2349
+ - GrassEnvironmentConfig (interface): interface GrassEnvironmentConfig
2350
+ - OceanEnvironmentConfig (interface): interface OceanEnvironmentConfig
2351
+ - SkyEnvironmentConfig (interface): interface SkyEnvironmentConfig
2352
+ - BuildingEnvironmentConfig (interface): interface BuildingEnvironmentConfig
2353
+ - PadSize (type): type PadSize = readonly [number, number] | { radius: number }
2354
+ - PadEnvironmentConfig (interface): interface PadEnvironmentConfig
2355
+ - TerrainEnvironmentDescriptor (type): type TerrainEnvironmentDescriptor = { kind: "terrain" } & Required< Pick<TerrainEnvironmentConfig, "bounds" | "height"> > & Omit<TerrainEnvironmentConfig, "bounds" | "height">
2356
+ - RainEnvironmentDescriptor (type): type RainEnvironmentDescriptor = { kind: "rain" } & Required< Pick<RainEnvironmentConfig, "area" | "density" | "speed" | "dropLength" | "wind" | "color"> >
2357
+ - SnowEnvironmentDescriptor (type): type SnowEnvironmentDescriptor = { kind: "snow" } & Required< Pick<SnowEnvironmentConfig, "area" | "density" | "speed" | "flakeSize" | "drift" | "wind" | "color"> >
2358
+ - GrassEnvironmentDescriptor (type): type GrassEnvironmentDescriptor = { kind: "grass" } & Required< Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors"> > & Pick<GrassEnvironmentConfig, "seed">
2359
+ - OceanEnvironmentDescriptor (type): type OceanEnvironmentDescriptor = { kind: "ocean" } & Required< Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color"> > & Pick<OceanEnvironmentConfig, "position">
2360
+ - BuildingEnvironmentDescriptor (type): type BuildingEnvironmentDescriptor = { kind: "building" } & Required< Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style"> > & Pick<BuildingEnvironmentConfig, "seed" | "position">
2361
+ - PadEnvironmentDescriptor (type): type PadEnvironmentDescriptor = { kind: "pad" } & Required< Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color"> > & Pick<PadEnvironmentConfig, "rotationY">
2362
+ - SkyEnvironmentDescriptor (type): type SkyEnvironmentDescriptor = { kind: "sky" } & Required< Pick<SkyEnvironmentConfig, "preset" | "timeOfDay"> > & Omit<SkyEnvironmentConfig, "preset" | "timeOfDay">
2363
+ - WeatherEnvironmentDescriptor (type): type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor
2364
+ - VegetationEnvironmentDescriptor (type): type VegetationEnvironmentDescriptor = GrassEnvironmentDescriptor
2365
+ - WaterEnvironmentDescriptor (type): type WaterEnvironmentDescriptor = OceanEnvironmentDescriptor
2366
+ - StructureEnvironmentDescriptor (type): type StructureEnvironmentDescriptor = BuildingEnvironmentDescriptor
2367
+ - EnvironmentDescriptorList (type): type EnvironmentDescriptorList<T> = T | readonly T[]
2368
+ - EnvironmentWorldConfig (interface): interface EnvironmentWorldConfig
2369
+ - EnvironmentWorldFeature (interface): interface EnvironmentWorldFeature
2370
+ - WorldGridCell (interface): interface WorldGridCell
2371
+ - WorldGridConfig (interface): interface WorldGridConfig — Shared by `biomes()`/`voxel()`/`plots()`/`tilemap()` so the shell can render their declared content as instanced boxes without a hand-written renderer.
2372
+ - BiomesWorldConfig (interface): interface BiomesWorldConfig extends WorldGridConfig
2373
+ - VoxelWorldConfig (interface): interface VoxelWorldConfig extends WorldGridConfig
2374
+ - PlotsWorldConfig (interface): interface PlotsWorldConfig extends WorldGridConfig
2375
+ - TilemapWorldConfig (interface): interface TilemapWorldConfig extends WorldGridConfig
2376
+ - WorldFeature (type): type WorldFeature = | ({ kind: "biomes" } & BiomesWorldConfig) | ({ kind: "voxel" } & VoxelWorldConfig) | ({ kind: "plots" } & PlotsWorldConfig) | ({ kind: "tilemap" } & TilemapWorldConfig) | EnvironmentWorldFeature | { kind: "flat" }
2377
+
2378
+ ### @jgengine/core/world/fog
2379
+
2380
+ - createFogField (function): function createFogField(config: FogConfig): FogField
2381
+ - FogBounds (interface): interface FogBounds
2382
+ - FogConfig (interface): interface FogConfig
2383
+ - FogCells (interface): interface FogCells
2384
+ - FogField (interface): interface FogField — Reveal-on-event fog of war over a fixed grid. Walking (`revealAlong`) and digging/acting (`reveal`) clear cells; once revealed a cell stays revealed. Pure and renderer-free — the shell/react map draws `cells()`.
2385
+
2386
+ ### @jgengine/core/world/geometry
2387
+
2388
+ - snapToGrid (function): function snapToGrid(point: Vec2, size: number): Vec2
2389
+ - footprintAabb (function): function footprintAabb(center: Vec2, footprint: Footprint, quarterTurns = 0): Aabb
2390
+ - expandAabb (function): function expandAabb(aabb: Aabb, margin: number): Aabb
2391
+ - aabbOverlap (function): function aabbOverlap(a: Aabb, b: Aabb): boolean
2392
+ - aabbContains (function): function aabbContains(outer: Aabb, inner: Aabb): boolean
2393
+ - pointInAabb (function): function pointInAabb(point: Vec2, aabb: Aabb): boolean
2394
+ - clampToAabb (function): function clampToAabb(point: Vec2, aabb: Aabb): Vec2
2395
+ - resolveMove (function): function resolveMove(from: Vec2, delta: Vec2, blockers: readonly Aabb[], options: MoveOptions = {}): Vec2
2396
+ - Vec2 (type): type Vec2 = readonly [number, number]
2397
+ - Footprint (interface): interface Footprint
2398
+ - Aabb (interface): interface Aabb
2399
+ - MoveOptions (interface): interface MoveOptions
2400
+
2401
+ ### @jgengine/core/world/gridInstances
2402
+
2403
+ - resolveGridInstances (function): function resolveGridInstances(config: WorldGridConfig): readonly GridInstanceTransform[] — Pure cell→instance math for `biomes()`/`voxel()`/`plots()`/`tilemap()` world features: one extruded box per declared cell, grounded at y=0 and centered on its cell in x/z. Consumed by the shell's grid-world renderer so it can drive a single `THREE.InstancedMesh` without duplicating this math per game.
2404
+ - GridInstanceTransform (interface): interface GridInstanceTransform
2405
+
2406
+ ### @jgengine/core/world/interiors
2407
+
2408
+ - createInteriors (function): function createInteriors(config: InteriorsConfig = {}): Interiors
2409
+ - SpaceRef (type): type SpaceRef = { kind: "exterior" } | { kind: "interior"; id: string }
2410
+ - EntityLocation (interface): interface EntityLocation
2411
+ - Interior (interface): interface Interior
2412
+ - Exterior (interface): interface Exterior
2413
+ - InteriorsConfig (interface): interface InteriorsConfig
2414
+ - Interiors (interface): interface Interiors
2415
+
2416
+ ### @jgengine/core/world/markers
2417
+
2418
+ - markerKindStyle (function): function markerKindStyle(kind: string, styles: Record<string, MarkerKindStyle> = DEFAULT_MARKER_KINDS): MarkerKindStyle
2419
+ - createMarkerSet (function): function createMarkerSet<TMeta = unknown>(now: () => number = Date.now): MarkerSet<TMeta>
2420
+ - MarkerPosition (type): type MarkerPosition = readonly [number, number, number]
2421
+ - MapMarker (interface): interface MapMarker<TMeta = unknown>
2422
+ - MarkerInput (interface): interface MarkerInput<TMeta = unknown>
2423
+ - MarkerQuery (interface): interface MarkerQuery
2424
+ - MarkerKindStyle (interface): interface MarkerKindStyle — Visual descriptor for a marker kind. Games supply their own palette; the engine ships `DEFAULT_MARKER_KINDS` as a content-agnostic starting set that the react minimap/compass read for colors and glyphs.
2425
+ - DEFAULT_MARKER_KINDS (const): const DEFAULT_MARKER_KINDS: Record<string, MarkerKindStyle>
2426
+ - MarkerSet (interface): interface MarkerSet<TMeta = unknown>
2427
+
2428
+ ### @jgengine/core/world/minimap
2429
+
2430
+ - compassBearing (function): function compassBearing(from: WorldXZ, to: WorldXZ): number — Compass bearing (radians, 0 = map north = −Z, increasing clockwise toward +X = east) from one world XZ point to another. Feeds both the minimap direction and the compass strip.
2431
+ - headingToBearing (function): function headingToBearing(yaw: number): number — Bearing of an entity facing direction given its `rotationY` (yaw) in radians.
2432
+ - bearingToCardinal (function): function bearingToCardinal(bearing: number): Cardinal
2433
+ - relativeBearing (function): function relativeBearing(bearing: number, reference: number): number — Signed offset of `bearing` from `reference`, wrapped into (−π, π].
2434
+ - projectToMinimap (function): function projectToMinimap(world: WorldXZ | readonly [number, number, number], view: MinimapView): MinimapPoint — Project a world XZ (or XYZ) point into minimap pixel space. Origin is the top-left of the `size×size` box; north (−Z) maps to −Y (up). Pass `view.rotate` to spin the map under a fixed north-up player arrow.
2435
+ - clampToMinimapEdge (function): function clampToMinimapEdge(point: MinimapPoint, size: number): { x: number; y: number } — Clamp a projected point to the minimap edge, preserving direction (edge markers).
2436
+ - WorldXZ (type): type WorldXZ = readonly [number, number]
2437
+ - Cardinal (type): type Cardinal = "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW"
2438
+ - MinimapView (interface): interface MinimapView
2439
+ - MinimapPoint (interface): interface MinimapPoint
2440
+
2441
+ ### @jgengine/core/world/placedStructureStore
2442
+
2443
+ - createPlacedStructureStore (function): function createPlacedStructureStore(): PlacedStructureStore
2444
+ - StructureVec3 (type): type StructureVec3 = readonly [number, number, number]
2445
+ - PlacedStructure (interface): interface PlacedStructure
2446
+ - AddStructureInput (interface): interface AddStructureInput
2447
+ - StructureFilter (interface): interface StructureFilter
2448
+ - PlacedStructureSnapshot (interface): interface PlacedStructureSnapshot
2449
+ - PlacedStructureStore (interface): interface PlacedStructureStore
2450
+
2451
+ ### @jgengine/core/world/placement
2452
+
2453
+ - validatePlacement (function): function validatePlacement(request: PlacementRequest, rules: PlacementRules = {}): PlacementResult
2454
+ - footprintObstacle (function): function footprintObstacle(request: PlacementRequest, id?: string): PlacementObstacle
2455
+ - PlacementObstacle (interface): interface PlacementObstacle
2456
+ - PlacementRequest (interface): interface PlacementRequest
2457
+ - PlacementRules (interface): interface PlacementRules
2458
+ - PlacementResult (type): type PlacementResult = | { status: "ok"; center: Vec2; aabb: Aabb } | { status: "rejected"; reason: "out-of-bounds" } | { status: "rejected"; reason: "overlap"; obstacle: PlacementObstacle; index: number }
2459
+
2460
+ ### @jgengine/core/world/placementController
2461
+
2462
+ - quarterTurnsToRotationY (function): function quarterTurnsToRotationY(quarterTurns: number): number
2463
+ - createPlacementController (function): function createPlacementController(config: PlacementControllerConfig): PlacementController
2464
+ - SnapMode (type): type SnapMode = "grid" | "free" | "surface"
2465
+ - PlacementVec3 (type): type PlacementVec3 = readonly [number, number, number]
2466
+ - PlacementHit (interface): interface PlacementHit
2467
+ - PlacementSlot (interface): interface PlacementSlot — A fixed buildable slot the placement controller snaps to instead of grid/free positioning.
2468
+ - PlacementControllerConfig (interface): interface PlacementControllerConfig
2469
+ - PlacementPreview (interface): interface PlacementPreview
2470
+ - PlacementCommit (interface): interface PlacementCommit
2471
+ - PlacementController (interface): interface PlacementController
2472
+
2473
+ ### @jgengine/core/world/realm
2474
+
2475
+ - composeRealm (function): function composeRealm(base: RealmBase, cards: readonly RealmCard[]): ComposedRealm — Assemble a played realm instance at runtime from a deck of modifier cards — the Nightingale "realm card" model. A major card is the biome base; minor cards layer weather, day length, and spawn edits. The result recomposes both the environment (into a sampleable field via `environmentField()`) and the spawn table, and it depends on the weather hooks in this group (#92) to turn its `weather` into gameplay modifiers. Cards apply in array order; sort your deck (majors first) before composing.
2476
+ - RealmEnvironmentParams (interface): interface RealmEnvironmentParams — The recomposable environment parameters a realm card can override.
2477
+ - DEFAULT_REALM_ENVIRONMENT (const): const DEFAULT_REALM_ENVIRONMENT: RealmEnvironmentParams
2478
+ - SpawnTableOverride (interface): interface SpawnTableOverride — Spawn-table edits a card layers on: replace weights, add/scale, or remove ids.
2479
+ - RealmCard (interface): interface RealmCard
2480
+ - RealmBase (interface): interface RealmBase
2481
+ - ComposedRealm (interface): interface ComposedRealm
2482
+
2483
+ ### @jgengine/core/world/regions
2484
+
2485
+ - createRegionField (function): function createRegionField<T = unknown>(config: RegionFieldConfig<T>): RegionField<T>
2486
+ - isRegionField (function): function isRegionField(field: TerrainField): field is RegionField
2487
+ - Rgb (type): type Rgb = readonly [number, number, number]
2488
+ - ColorInput (type): type ColorInput = string | Rgb
2489
+ - RegionHeight (interface): interface RegionHeight
2490
+ - RegionDef (interface): interface RegionDef<T = unknown> — A content-agnostic region. The engine blends `height` and the visual/physical knobs (`tint`, `water`, `fog`, `speedMultiplier`, …) across region borders and selects the dominant region by nearest center in `selector` space. `data` is an opaque payload the engine never inspects — games hang their own content off it (spawn tables, prop palettes, structure ids, names, …).
2491
+ - RegionSample (interface): interface RegionSample<T = unknown>
2492
+ - RegionField (interface): interface RegionField<T = unknown> extends TerrainField
2493
+ - RegionFieldConfig (interface): interface RegionFieldConfig<T = unknown>
2494
+
2495
+ ### @jgengine/core/world/scatter
2496
+
2497
+ - scatterAabb (function): function scatterAabb(area: ScatterArea | Aabb): Aabb
2498
+ - scatter (function): function scatter(config: ScatterConfig): ScatterPoint[]
2499
+ - ScatterArea (interface): interface ScatterArea
2500
+ - ScatterConfig (interface): interface ScatterConfig
2501
+ - ScatterPoint (interface): interface ScatterPoint
2502
+
2503
+ ### @jgengine/core/world/scatterItems
2504
+
2505
+ - scatterItems (function): function scatterItems<T>(field: RegionField<T>, area: Aabb, layersFor: (sample: RegionSample<T>) => readonly ScatterLayer[], options: { cell?: number; max?: number; saltKey?: number } = {}): ScatterInstance[] — Deterministically place opaque items across `area`, grounded on a region field. For each grid cell it asks `layersFor` which items may appear in that region and rolls one against their densities. The engine never interprets `item` — a game maps it to a mesh or entity. Content scatter (region-driven density) as opposed to `scatter` in `./scatter`, which is renderer-free geometric point distribution.
2506
+ - pickWeighted (function): function pickWeighted<T>(entries: readonly { value: T; weight: number }[], roll: number): T | null — Weighted pick from opaque entries; `roll` in [0, 1). Returns null when empty.
2507
+ - ScatterLayer (interface): interface ScatterLayer — One placeable class. `item` is an opaque id the caller maps to a mesh/entity.
2508
+ - ScatterInstance (interface): interface ScatterInstance
2509
+
2510
+ ### @jgengine/core/world/support
2511
+
2512
+ - solveSupport (function): function solveSupport(pieces: readonly SupportPiece[], links: readonly SupportLink[], config: SupportConfig = {}): SupportResult
2513
+ - toDebrisBodies (function): function toDebrisBodies(pieces: readonly SupportPiece[], collapsedIds: readonly string[], options: DebrisOptions = {}): AddBodyOptions[]
2514
+ - SupportPiece (interface): interface SupportPiece
2515
+ - SupportLink (interface): interface SupportLink
2516
+ - SupportConfig (interface): interface SupportConfig
2517
+ - SupportResult (interface): interface SupportResult
2518
+ - DebrisOptions (interface): interface DebrisOptions
2519
+
2520
+ ### @jgengine/core/world/terraform
2521
+
2522
+ - brushWeight (function): function brushWeight(distance: number, radius: number, falloff: TerraformFalloff): number
2523
+ - createEditableTerrain (function): function createEditableTerrain(config: EditableTerrainConfig): EditableTerrain
2524
+ - createTerraformBrush (function): function createTerraformBrush(terrain: Pick<EditableTerrain, "apply">, config: TerraformBrushConfig = {}): TerraformBrush
2525
+ - TerraformMode (type): type TerraformMode = "raise" | "lower" | "flatten" | "paint"
2526
+ - TerraformFalloff (type): type TerraformFalloff = "smooth" | "linear" | "none"
2527
+ - TerraformEdit (interface): interface TerraformEdit
2528
+ - EditableTerrainConfig (interface): interface EditableTerrainConfig
2529
+ - TerraformSnapshot (interface): interface TerraformSnapshot
2530
+ - EditableTerrain (interface): interface EditableTerrain extends TerrainField
2531
+ - TerraformBrushConfig (interface): interface TerraformBrushConfig
2532
+ - TerraformBrush (interface): interface TerraformBrush
2533
+
2534
+ ### @jgengine/core/world/terrain
2535
+
2536
+ - valueNoise (function): function valueNoise(x: number, z: number, seed: number): number
2537
+ - fractalNoise (function): function fractalNoise(x: number, z: number, config: FractalNoiseConfig): number
2538
+ - seedFrom (function): function seedFrom(value: string | number | undefined, fallback: number): number
2539
+ - withNormal (function): function withNormal(sampleHeight: (x: number, z: number) => number): TerrainField["sampleNormal"]
2540
+ - flatField (function): function flatField(): TerrainField
2541
+ - noiseField (function): function noiseField(config: NoiseFieldConfig = {}): TerrainField
2542
+ - rollingField (function): function rollingField(config: RollingFieldConfig = {}): TerrainField
2543
+ - arenaField (function): function arenaField(config: ArenaFieldConfig = {}): TerrainField
2544
+ - groundFieldFor (function): function groundFieldFor(world?: WorldFeature): TerrainField
2545
+ - resolveTerrainPalette (function): function resolveTerrainPalette(descriptor: Pick<TerrainEnvironmentConfig, "material" | "colors"> = {}): TerrainPalette
2546
+ - resolveTerrainField (function): function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField
2547
+ - snapToGround (function): function snapToGround(field: TerrainField, position: readonly [number, number, number], offset = 0): [number, number, number] — Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`.
2548
+ - snapEntityToGround (function): function snapEntityToGround(entities: GroundSnapEntityStore, id: string, field: TerrainField, offset = 0): boolean — Ground-snaps an already-spawned entity in place; returns false when `id` is unknown.
2549
+ - resolveGroundStep (function): function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope = DEFAULT_MAX_WALK_SLOPE): { stepX: number; stepZ: number }
2550
+ - TerrainNormal (type): type TerrainNormal = readonly [number, number, number]
2551
+ - TerrainField (interface): interface TerrainField
2552
+ - FractalNoiseConfig (interface): interface FractalNoiseConfig
2553
+ - FLAT_FIELD (const): const FLAT_FIELD: TerrainField
2554
+ - NoiseFieldConfig (interface): interface NoiseFieldConfig
2555
+ - RollingFieldConfig (interface): interface RollingFieldConfig
2556
+ - ArenaFieldConfig (interface): interface ArenaFieldConfig
2557
+ - ARENA_WATER_LEVEL (const): const ARENA_WATER_LEVEL: -0.9
2558
+ - TerrainPalette (interface): interface TerrainPalette
2559
+ - DEFAULT_TERRAIN_MATERIAL (const): const DEFAULT_TERRAIN_MATERIAL: "grass"
2560
+ - TERRAIN_MATERIAL_PALETTES (const): const TERRAIN_MATERIAL_PALETTES: Record<string, TerrainPalette>
2561
+ - GroundSnapTarget (interface): interface GroundSnapTarget
2562
+ - GroundSnapEntityStore (interface): interface GroundSnapEntityStore
2563
+ - DEFAULT_MAX_WALK_SLOPE (const): const DEFAULT_MAX_WALK_SLOPE: 0.6
2564
+
2565
+ ### @jgengine/core/world/voxelField
2566
+
2567
+ - createVoxelField (function): function createVoxelField<T extends string = string>(config?: VoxelFieldConfig): VoxelField<T>
2568
+ - VoxelCoord (type): type VoxelCoord = readonly [number, number, number]
2569
+ - VoxelFace (type): type VoxelFace = "px" | "nx" | "py" | "ny" | "pz" | "nz"
2570
+ - VoxelFieldConfig (interface): interface VoxelFieldConfig
2571
+ - VoxelCell (interface): interface VoxelCell<T extends string = string>
2572
+ - VoxelHit (interface): interface VoxelHit<T extends string = string>
2573
+ - VoxelBounds (interface): interface VoxelBounds
2574
+ - VoxelFieldSummary (interface): interface VoxelFieldSummary<T extends string = string>
2575
+ - VoxelField (interface): interface VoxelField<T extends string = string>
2576
+ - VOXEL_FACES (const): const VOXEL_FACES: readonly VoxelFace[]
2577
+ - VOXEL_FACE_NORMALS (const): const VOXEL_FACE_NORMALS: Record<VoxelFace, VoxelCoord>
2578
+
2579
+ ### @jgengine/core/world/walls
2580
+
2581
+ - wallSegments (function): function wallSegments(points: readonly Vec2[], closed: boolean): WallSegment[]
2582
+ - isEnclosed (function): function isEnclosed(points: readonly Vec2[], tolerance = 0.5): boolean
2583
+ - enclosePath (function): function enclosePath(points: readonly Vec2[], tolerance = 0.5): readonly Vec2[]
2584
+ - polygonArea (function): function polygonArea(polygon: readonly Vec2[]): number
2585
+ - footprintFromWalls (function): function footprintFromWalls(points: readonly Vec2[], tolerance = 0.5): EnclosedFootprint | null
2586
+ - autoRoof (function): function autoRoof(footprint: EnclosedFootprint, config: RoofConfig = {}): RoofPlan
2587
+ - createSurfacePaint (function): function createSurfacePaint(): SurfacePaintStore
2588
+ - createWallDrawTool (function): function createWallDrawTool(config: { snap?: number; closeTolerance?: number } = {}): WallDrawTool
2589
+ - wallSegmentBounds (function): function wallSegmentBounds(segment: WallSegment, thickness: number): Aabb
2590
+ - WallVec3 (type): type WallVec3 = readonly [number, number, number]
2591
+ - WallSegment (interface): interface WallSegment
2592
+ - EnclosedFootprint (interface): interface EnclosedFootprint
2593
+ - RoofStyle (type): type RoofStyle = "hip" | "gable" | "flat"
2594
+ - RoofPlan (interface): interface RoofPlan
2595
+ - RoofConfig (interface): interface RoofConfig
2596
+ - PaintTarget (type): type PaintTarget = "floor" | "wall"
2597
+ - SurfacePaintStore (interface): interface SurfacePaintStore
2598
+ - WallDrawTool (interface): interface WallDrawTool
2599
+
2600
+ ### @jgengine/core/world/water
2601
+
2602
+ - synthesizeWaves (function): function synthesizeWaves(config: WaterSurfaceConfig = {}): GerstnerWave[]
2603
+ - waterSurface (function): function waterSurface(config: WaterSurfaceConfig = {}): WaterSurface
2604
+ - waterSurfaceFromDescriptor (function): function waterSurfaceFromDescriptor(descriptor: OceanEnvironmentDescriptor, waves?: number): WaterSurface
2605
+ - WaterNormal (type): type WaterNormal = readonly [number, number, number]
2606
+ - GerstnerWave (interface): interface GerstnerWave
2607
+ - WaterSurfaceConfig (interface): interface WaterSurfaceConfig
2608
+ - WaterSurface (interface): interface WaterSurface
2609
+
2610
+ ### @jgengine/core/world/weather
2611
+
2612
+ - resolveWeather (function): function resolveWeather(state: WeatherState, table: WeatherModifierTable): ResolvedWeather — Resolve a weather state against a game-owned modifier table into concrete gameplay numbers. Multiplier-style effects (grip, visibility, spread) interpolate from the neutral 1 by intensity; rate-style effects (structureDamage, ignition, chill) scale linearly. Games read `grip`/`visibility` in movement and AI, `structureDamage` on a building tick, and `spread`/`ignition` feed the fire grid.
2613
+ - createFireGrid (function): function createFireGrid(config: FireGridConfig): FireGrid
2614
+ - WeatherKind (type): type WeatherKind = string
2615
+ - WeatherState (interface): interface WeatherState
2616
+ - WeatherModifier (interface): interface WeatherModifier — Per-kind gameplay effect values at full intensity. Intensity interpolates from neutral.
2617
+ - WeatherModifierTable (type): type WeatherModifierTable = Record<WeatherKind, WeatherModifier>
2618
+ - ResolvedWeather (interface): interface ResolvedWeather
2619
+ - FireCellState (type): type FireCellState = "unburnt" | "burning" | "burnt"
2620
+ - FireCell (interface): interface FireCell
2621
+ - FireGridConfig (interface): interface FireGridConfig
2622
+ - FireStepOptions (interface): interface FireStepOptions
2623
+ - FireGrid (interface): interface FireGrid
2624
+
2625
+ ### @jgengine/core/world/wind
2626
+
2627
+ - windField (function): function windField(config: WindFieldConfig = {}): WindField
2628
+ - WindVector (type): type WindVector = readonly [number, number]
2629
+ - WindFieldConfig (interface): interface WindFieldConfig
2630
+ - WindField (interface): interface WindField
2631
+
2632
+ ## Guides
2633
+
2634
+ # JGengine — API Reference
2635
+
2636
+ The engine ships **verbs and primitives**; your game ships **nouns** (catalogs) and thin handlers. Read this before writing `game.config.ts` or any game content. Companion skills: **`jgengine-newgame`** (master blueprint + phased build to completion) and **`jgengine-verify`** (browserless scene gate) — read them before building. The UI quality bar lives in [`reference/ui-react.md`](reference/ui-react.md); asset sourcing lives in the **Assets** section below.
2637
+
2638
+ The heaviest domains live in on-demand reference modules under [`reference/`](reference/) — load one only when you're building in that domain: [`reference/combat.md`](reference/combat.md) (effects, projectiles, death, feel, abilities), [`reference/world.md`](reference/world.md) (terrain, environment, physics, vehicles, spawn), [`reference/multiplayer.md`](reference/multiplayer.md) (transport, host, persistence, presence), [`reference/ui-react.md`](reference/ui-react.md) (`@jgengine/react` hooks, headless + styled UI kits, the registry install path, and the UI quality bar). Each domain's section below is a one-line pointer to its module.
2639
+
2640
+ ## Packages
2641
+
2642
+ All published on npm, source at [github.com/Noisemaker111/jgengine](https://github.com/Noisemaker111/jgengine) (AGPL-3.0):
2643
+
2644
+ | Package | Role | May import |
2645
+ |---------|------|------------|
2646
+ | `@jgengine/core` | Everything below: defineGame, GameContext, scene, combat, game systems, movement, input, world features, runtime/transport contracts | nothing platform-specific — no React, Convex, three.js, browser |
2647
+ | `@jgengine/react` | `GameProvider`, hooks, headless UI primitives | react + core |
2648
+ | `@jgengine/shell` | `GamePlayerShell` — R3F canvas, camera rig library (orbit, first-person, top-down/iso, RTS, over-the-shoulder, lock-on, chase, cinematic + shake channel), input tracking, HUD mounting, `GameUiPreview`, demo game; you supply a `GameRegistry` | react + three + core |
2649
+ | `@jgengine/ws` | Browser-safe `GameBackend` over a pluggable string-pipe transport (WebSocket/socket.io/WebRTC/loopback), protocol codec, HTTP reads, browser-safe authoritative host + router (`host`/`hostRouter`), and P2P (`peer`) | core |
2650
+ | `@jgengine/node` | Node process bindings over `@jgengine/ws`'s host/router: `ws`-package server, socket.io server attach, file persistence (re-exports `createGameHost`/`memoryPersistence` unchanged) | node + ws + core |
2651
+ | `@jgengine/sql` | `HostPersistence` on Postgres (structural pool, no hard `pg` dep) | core |
2652
+ | `@jgengine/convex` | The Convex **adapter** behind the `GameBackend` seam | react + convex + core |
2653
+
2654
+ Import by deep path: `@jgengine/core/<domain>/<file>` (e.g. `@jgengine/core/runtime/gameContext`).
2655
+
2656
+ ## Hit a snag? File an issue
2657
+
2658
+ Any hiccup with JGengine — a doc that's wrong, a missing primitive, a rough edge, or a feature/improvement idea — file it fast at [github.com/Noisemaker111/jgengine/issues](https://github.com/Noisemaker111/jgengine/issues). A 30-second issue (what you were building, the glue it forced, the API you wanted) is worth more than a silent workaround — that's the fastest way gaps and doc errors get closed. Don't reverse-engineer around a broken doc in silence; report it.
2659
+
2660
+ ## Upgrading? Read the changelog
2661
+
2662
+ All eight packages version in lockstep. When you bump (e.g. `0.6` → `0.7`) to pick up new capabilities, read [`CHANGELOG.md`](https://github.com/Noisemaker111/jgengine/blob/main/CHANGELOG.md) — each release leads with a **Migrate** block listing the concrete steps to move a game onto the new APIs. It ships inside every package too (`node_modules/@jgengine/core/CHANGELOG.md`), and as typed values: `import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` to diff your installed version against the latest programmatically.
2663
+
2664
+ ## Concept → Type Reference
2665
+
2666
+ Exact import paths and export names — **do not invent paths**; every row below resolves to a real file under `packages/core/src`. Import the deep path form `@jgengine/core/<path>`.
2667
+
2668
+ | Concept | Import path (`@jgengine/core/…`) | Export(s) |
2669
+ |---------|----------------------------------|-----------|
2670
+ | Game boot | `game/defineGame` | `defineGame`, `GameDefinition`, `GameLoop`, `InventoryDeclaration`, `PhysicsConfig`, `GameServerConfig`, `TimeConfig` |
2671
+ | Simulation clock | `time/simClock` | `createSimClock`, `SimClock`, `TimeConfig`, `ClockSnapshot`, `CalendarTime` |
2672
+ | Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
2673
+ | Runtime ctx | `runtime/gameContext` | `createGameContext`, `GameContext`, `GameContextContent`, `GameContextItemEntry`, `GameContextEntityEntry`, `GameContextObjectEntry`, `CatalogEntityRole` |
2674
+ | Behaviour lifecycle | `behaviour/behaviour` | `Behaviour` (`onAwake`→`onEnable`→`onStart`→`onUpdate(dt)`→`onDisable`→`onDestroy`), `BehaviourModule`, `createBehaviourWorld`, `BehaviourWorld`, `JGEngineRegister`, `RegisterField`, `BehaviourModules` — Unity-style lifecycle over an id-keyed node tree (`setActive` cascade, lazy update dispatch); key nodes by entity instance ids. Games augment `JGEngineRegister` via `declare module "@jgengine/core/behaviour/behaviour"` for typed `world.modules`. Three.js binding: `Object3DBehaviour`, `attachObject3D`, `useBehaviourWorld` from `@jgengine/shell/behaviour` |
2675
+ | Reactive keyed store | `store/observableKeyedStore` | `createObservableKeyedStore`, `ObservableKeyedStore` — backs `ctx.game.store` |
2676
+ | Scene instance role | `scene/entityStore` | `EntityRole`, `SceneEntity`, `SpawnOptions`, `EntityPose` |
2677
+ | Object spatial queries | `scene/objectQuery` | `raycastObjects`, `raycastObjectsAll`, `ObjectRaycastInput`, `ObjectRaycastHit` — backs `ctx.scene.object.raycast`/`raycastAll` |
2678
+ | Runtime paint layer | `scene/paintLayer` | `createPaintLayer`, `PaintLayer`, `PaintStroke` — backs `ctx.scene.entity.paint` |
2679
+ | Possession | `scene/possession` | `createPossession`, `Possession`, `PossessionDeps`, `PossessionSwappedEvent` |
2680
+ | Form / shapeshift | `scene/form` | `createForms`, `Forms`, `FormDef`, `FormsDeps`, `FormChangedEvent` |
2681
+ | Multiplayer adapters | `runtime/adapter` | `offline`, `ws`, `convex`, `socketIo`, `p2p`, `lan`, `fly`, `servers`, `MultiplayerTopology`, `ServersPoolConfig` |
2682
+ | Loot | `game/lootTable` | `lootTable`, `LootTableDef`, `LootEntry`, `Drop` |
2683
+ | Dropped-item entity | `game/worldItem` | `WORLD_ITEM_ENTITY_NAME`, `WorldItemRecord`, `WorldItemSpawnInput`, `createWorldItemStore`, `resolveDeathDrops`, `scatterOffset`, `scatterPosition`, `selectNearestWorldItem`, `resolveWorldItemPresentation`, `RarityStyle`, `WorldItemPresentation`, `DEFAULT_RARITY`, `DEFAULT_PICKUP_RADIUS`, `DEFAULT_SCATTER` |
2684
+ | Loot filter | `game/lootFilter` | `lootFilter`, `evaluateLootFilter`, `LootFilterRule`, `LootFilterCondition`, `LootFilterItem`, `LootFilterOverride` |
2685
+ | Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
2686
+ | Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
2687
+ | Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
2688
+ | World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
2689
+ | Voxel field | `world/voxelField` | `createVoxelField`, `VoxelField`, `VoxelCell`, `VoxelHit`, `VoxelBounds`, `VoxelFieldSummary`, `VoxelFace`, `VOXEL_FACES`, `VOXEL_FACE_NORMALS` — a chunked block lattice, distinct from the `voxel()` `WorldFeature` descriptor |
2690
+ | Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
2691
+ | Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
2692
+ | Name generator | `random/nameGen` | `createNameGenerator`, `pickFrom`, `fillTemplate`, `NameGenerator`, `NameGeneratorOptions`, `SyllableBank` |
2693
+ | Regions | `world/regions` | `createRegionField`, `isRegionField`, `RegionDef`, `RegionField`, `RegionSample` |
2694
+ | Wind field | `world/wind` | `windField`, `WindField`, `WindFieldConfig`, `WindVector` |
2695
+ | Water surface | `world/water` | `waterSurface`, `waterSurfaceFromDescriptor`, `synthesizeWaves`, `WaterSurface`, `GerstnerWave` |
2696
+ | Scatter | `world/scatter` | `scatter`, `scatterAabb`, `ScatterConfig`, `ScatterPoint` |
2697
+ | Content scatter | `world/scatterItems` | `scatterItems`, `pickWeighted`, `ScatterLayer`, `ScatterInstance` |
2698
+ | Building generator | `world/buildings` | `generateBuilding`, `generateBuildingDistrict`, `createBuildingGrid`, `GeneratedBuilding` |
2699
+ | Building index | `world/buildingIndex` | `buildingIndex`, `BuildingIndex`, `BuildingHit` |
2700
+ | Scene summary | `world/environmentSummary` | `summarizeEnvironment`, `resolveStructureBuildings`, `EnvironmentSummary` |
2701
+ | Map markers | `world/markers` | `createMarkerSet`, `MarkerSet`, `MapMarker`, `MarkerInput`, `MarkerKindStyle`, `DEFAULT_MARKER_KINDS`, `markerKindStyle` |
2702
+ | Fog of war | `world/fog` | `createFogField`, `FogField`, `FogConfig`, `FogBounds`, `FogCells` |
2703
+ | Minimap math | `world/minimap` | `projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`, `MinimapView` |
2704
+ | Ping | `game/ping` | `createPingSystem`, `classifyPing`, `PingSystem`, `PingPayload`, `PingCategory`, `PingCategoryDef`, `DEFAULT_PING_CATEGORIES`, `PING_FEED_ACTION` |
2705
+ | Proximity prompt | `interaction/proximityPrompt` | `proximityPrompt`, `ProximityPrompt`, `ProximityPromptDisplay`, `keybind`, `gauge`, `label`, `command` |
2706
+ | Skill-check minigame | `interaction/skillCheck` | `evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`, `SkillCheckConfig`, `SkillCheckZone`, `SkillCheckResult` |
2707
+ | QTE sequencer | `interaction/qte` | `evaluateQteSequence`, `pendingQteStep`, `qteProgress`, `QteStep`, `QteInputEvent`, `QteOutcome` |
2708
+ | Item use | `item/use` | `createItemUse`, `ItemUseHandler`, `ItemUseInput`, `ItemUseResult`, `ItemUseRejection` |
2709
+ | Durability | `item/durability` | `createDurability`, `wear`, `repairQuote`, `isDisabled`, `createDurabilityTracker`, `DurabilitySpec`, `DurabilityState`, `RepairSpec`, `RepairQuote` |
2710
+ | Affix roller | `item/affix` | `createAffixRoller`, `seededRng`, `AffixRoller`, `RollerConfig`, `AffixPool`, `AffixDef`, `RarityTier`, `ItemBaseDef`, `RolledItem`, `RolledAffix` |
2711
+ | Modular item | `item/modularItem` | `createModularItem`, `install`, `computeEffectiveStats`, `missingRequiredSlots`, `ModularItemDef`, `MountSlotDef`, `PartDef`, `InstalledPart` |
2712
+ | Storage tier | `inventory/storageTier` | `partitionOnDeath`, `createDeliveryQueue`, `insureLost`, `resolveConsolation`, `tierOf`, `StorageTier`, `ContainerSnapshot`, `DeathPartition`, `DeliveryQueue`, `InsurancePolicy`, `ConsolationPolicy` |
2713
+ | Contested channel | `session/contestedChannel` | `createContestedChannel`, `ContestedChannel`, `ContestedChannelConfig`, `ContestedEvent`, `ContestedPhase`, `ContestedSnapshot` |
2714
+ | Round state | `session/roundState` | `createRoundState`, `lossBonusFor`, `RoundState`, `RoundConfig`, `RoundPhase`, `RoundTeam`, `RoundEvent`, `RoundEconomy`, `RoundSnapshot`, `LossBonusRule` |
2715
+ | Shrinking ring | `session/ring` | `createRing`, `ringSampleAt`, `Ring`, `RingConfig`, `RingPhase`, `RingSample`, `RingHit`, `RingPoint` |
2716
+ | Extraction session | `session/extraction` | `createRaidSession`, `RaidSession`, `RaidSessionConfig`, `ExtractPoint`, `ExtractionResult`, `DeathResult`, `RaidStatus` |
2717
+ | Role assignment | `session/roles` | `assignRoles`, `RoleSpec` |
2718
+ | Downed / revive | `combat/downed` | `createDownedState`, `DownedState`, `DownedConfig`, `DownedPhase`, `DownedEntry`, `DownedEvent` |
2719
+ | Persistence scopes | `runtime/persistenceScope` | `partitionScopes`, `resetRun`, `mergeScopes`, `clearRunFields`, `applyRunReset`, `planScenarioReset`, `ScopeSchema`, `ScenarioReset`, `PersistenceScope` |
2720
+ | Inventory | `inventory/inventoryModel` | `InventoryLayout`, `InventorySet`, `ItemTraits` |
2721
+ | Progression | `game/progression` | `curve`, `evalCurve`, `leveling`, `Curve`, `LevelingTrack`, `LevelProgress` |
2722
+ | Inventory slots | `inventory/slotModel` | `createSlots`, `placeAt`, `removeAt`, `moveSlot`, `firstEmpty`, `compactSlots`, `Slot`, `SlotGrid` |
2723
+ | Shaped inventory | `inventory/shapedGrid` | `createShapedGrid`, `placeShaped`, `moveShaped`, `removeShaped`, `canPlace`, `rotateFootprint`, `occupiedCells`, `gridAdjacencyQuery`, `cellFromPoint`, `ShapedGrid`, `Footprint`, `Placement`, `Rotation` |
2724
+ | Card piles | `cards/cardPile` | `createCardPile`, `createCardPileState`, `draw`, `moveCards`, `shuffleZone`, `pileRng`, `CardPile`, `CardPileState`, `CardPileConfig` |
2725
+ | Modifier pipeline | `cards/modifierPipeline` | `createModifierPipeline`, `runPipeline`, `Modifier`, `TraceStep`, `PipelineResult` |
2726
+ | Lane board | `board/laneBoard` | `createLaneBoard`, `laneAggregate`, `laneOutcome`, `boardTotals`, `lanesWon`, `LaneBoard`, `LaneRule`, `LaneBoardConfig` |
2727
+ | Timeline board | `board/timelineBoard` | `createTimelineBoard`, `tickTimeline`, `TimelineBoard`, `TimelineSlot`, `TimelineFire` |
2728
+ | World geometry | `world/geometry` | `footprintAabb`, `aabbOverlap`, `snapToGrid`, `resolveMove`, `Aabb`, `Footprint` |
2729
+ | Placement | `world/placement` | `validatePlacement`, `footprintObstacle`, `PlacementRules`, `PlacementResult` |
2730
+ | Placement ghost | `world/placementController` | `createPlacementController`, `PlacementController`, `PlacementPreview`, `PlacementCommit`, `SnapMode`, `quarterTurnsToRotationY` |
2731
+ | Connector sockets | `world/connectors` | `snapToNearest`, `socketsCompatible`, `worldSockets`, `socketWorldPosition`, `ConnectorSocket`, `ConnectorPieceDef`, `PlacedPiece`, `SnapResult` |
2732
+ | Structural support | `world/support` | `solveSupport`, `toDebrisBodies`, `SupportPiece`, `SupportLink`, `SupportResult` |
2733
+ | Wall/roof authoring | `world/walls` | `createWallDrawTool`, `footprintFromWalls`, `autoRoof`, `wallSegments`, `createSurfacePaint`, `WallDrawTool`, `RoofPlan`, `EnclosedFootprint` |
2734
+ | Placed structures | `world/placedStructureStore` | `createPlacedStructureStore`, `PlacedStructure`, `PlacedStructureStore`, `PlacedStructureSnapshot` |
2735
+ | Terraform | `world/terraform` | `createEditableTerrain`, `createTerraformBrush`, `brushWeight`, `EditableTerrain`, `TerraformBrush`, `TerraformEdit`, `TerraformMode` |
2736
+ | Build permissions | `world/buildPermissions` | `createPlotPermissions`, `createContributionPool`, `PlotPermissions`, `ContributionPool`, `BuildRole`, `ContributionGoal` |
2737
+ | Interiors | `world/interiors` | `createInteriors`, `Interior`, `Exterior`, `SpaceRef` |
2738
+ | Game clock | `time/gameClock` | `getScaledElapsedMs`, `computeGameDay`, `SECONDS_PER_GAME_DAY` |
2739
+ | Scene behaviors | `scene/behaviors` | `wander`, `patrol`, `promptable`, `talkable`, `player` |
2740
+ | Capture check | `scene/captureCheck` | `captureChance`, `rollCapture`, `CaptureCheckInput` |
2741
+ | Owned roster | `scene/roster` | `createRoster`, `Roster`, `RosterEntry`, `RosterCaptureOptions` |
2742
+ | Economy wallet | `economy/wallet` | `createEmptyWallet`, `balance`, `grant`, `charge`, `canAfford`, `chargeAll` |
2743
+ | Tech tree | `economy/techTree` | `createTechTree`, `TechTree`, `TechNodeDef`, `canUnlockTech`, `availableTech`, `unlockedRecipes`, `grantTech`, `techPrerequisitesMet` |
2744
+ | Recipe graph | `crafting/recipe` | `createRecipeGraph`, `RecipeGraph`, `RecipeDef`, `RecipeItem`, `canCraft`, `craft`, `missingInputs`, `stationSatisfied`, `craftSeconds` |
2745
+ | Production building | `crafting/production` | `productionBuilding`, `ProductionBuildingDef`, `createProductionState`, `tickProduction`, `feedProduction`, `drainOutput`, `advanceTransport`, `resolvePowerGrid` |
2746
+ | Crop tile / farming | `crafting/crop` | `createCropField`, `CropField`, `CropDef`, `CropTileState`, `tillTile`, `plantCrop`, `waterTile`, `advanceCropDay`, `harvestCrop`, `applyToolToTiles`, `squarePattern`, `diamondPattern`, `createDayTicker` |
2747
+ | Skill-check roll | `stats/rollCheck` | `rollCheck`, `CheckInput`, `CheckResult`, `CheckAdvantage` |
2748
+ | Input bindings (full) | `input/actionBindings` | `hotbarSlotBindings`, `actionLabel`, `bindingLabel`, `resolveActionCommand`, `bindingMatches`, `createActionStateTracker` |
2749
+ | Touch controls | `input/touchScheme` | `deriveTouchScheme`, `touchCode`, `touchActionLabel`, `withTouchCodes`, `TouchControlsConfig`, `TouchGestureBindings`, `TouchDragBinding`, `TouchButtonSpec`, `TouchScheme`, `TouchJoystick`, `TouchButton` |
2750
+ | Pointer hit | `input/pointer` | `PointerHit`, `PointerButton`, `aimToPoint`, `moveTargetFromHit`, `groundOf`, `PointerVec3` |
2751
+ | Navmesh + A* | `nav/navGrid` | `createNavGrid`, `findPath`, `smoothPath`, `NavGrid`, `NavGridConfig`, `NavPoint`, `FindPathOptions` |
2752
+ | Path follow | `nav/pathFollow` | `createPathFollow`, `advancePathFollow`, `pathFromNav`, `PathFollowConfig`, `PathFollowState`, `Waypoint` |
2753
+ | Nav-grid movement constraint | `nav/navConstrain` | `constrainToNavGrid`, `NavConstrainProposed`, `NavConstrainEntity`, `NavConstrainOptions` — a standalone walkable-pass-through + wall-slide helper; adapt its `(proposed, entity)` shape to `PlayerMovementConfig.beforeCommit`'s `(frame) => [x,y,z]` with a small closure |
2754
+ | Selection set | `scene/selection` | `createSelectionSet`, `SelectionSet`, `screenRect`, `selectWithinRect`, `rectContainsPoint`, `isMarquee`, `ScreenRect` |
2755
+ | Context menu | `interaction/contextMenu` | `contextVerb`, `buildContextMenu`, `contextVerbInput`, `ContextVerb`, `ContextMenu` |
2756
+ | Shared / group wallet | `economy/sharedWallet` | `createWalletBook`, `WalletBook`, `WalletScope`, `userScope`, `groupScope`, `balanceIn`, `grantTo`, `chargeFrom`, `contributionOf`, `contributorsOf` |
2757
+ | Analog axis input | `input/axisInput` | `AxisInput`, `AxisChannel`, `AxisBindingMap`, `DRIVE_AXIS_BINDINGS`, `clampAxis`, `rampToward`, `NEUTRAL_AXIS` |
2758
+ | Raw control polling | `runtime/inputSnapshot` | `createInputSnapshot`, `InputSnapshot` — backs `ctx.input` |
2759
+ | Physics world | `physics/physicsWorld` | `PhysicsWorld`, `PhysicsWorldConfig`, `PhysicsBounds`, `PhysicsStats`, `AddBodyOptions` (`{ shape: "box", halfExtents }` \| `{ shape: "sphere", radius }`), `JointOptions`, `JointKind`, `CollisionEvent` |
2760
+ | Ballistic collision sweep | `physics/ballisticSweep` | `createBallisticSweep`, `BallisticSweep`, `BallisticSweepHit`, `BallisticSweepOptions` |
2761
+ | Tweening / easing | `anim/easing` | `Easing`, `lerp`, `clamp01`, `smoothstep`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `easeOutElastic`, `tween`, `timedProgress` |
2762
+ | Async data source | `data/dataSource` | `createDataSource`, `DataSource`, `DataSourceState`, `DataSourceStatus`, `DataSourceOptions`, `DataSourceClock`, `RefreshOptions` |
2763
+ | JSON fetch | `data/fetchJson` | `fetchJson`, `FetchJsonOptions`, `FetchImpl`, `HttpStatusError`, `JsonParseError` |
2764
+ | JSON data source | `data/jsonDataSource` | `createJsonDataSource`, `JsonDataSourceOptions` |
2765
+ | Dev proxy routing | `data/devProxy` | `proxiedUrl`, `parseDevProxyTable`, `DevProxyTable`, `ProxiedUrlOptions`, `DEFAULT_DEV_PROXY_PREFIX` |
2766
+ | Grid-cell world rendering | `world/gridInstances` | `resolveGridInstances`, `GridInstanceTransform` |
2767
+ | Turn loop | `turn/turnLoop` | `createTurnLoop`, `TurnLoop`, `TurnLoopConfig`, `TurnState`, `PoolConfig`, `PoolState`, `TurnLoopSnapshot` |
2768
+ | Declared-action intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` — `declare(participantId, intent)`, `peek`, `all`, `consume`, `clear` |
2769
+ | Commit modes | `turn/commit` | `createCommitController`, `CommitController`, `CommitMode`, `CommitOutcome`, `SubmittedAction` |
2770
+ | Intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` |
2771
+ | Tactical grid | `tactics/tacticalGrid` | `createTacticalGrid`, `TacticalGrid`, `TacticalGridConfig`, `Tile`, `ReachableTile`, `PushResult`, `PushCollision` |
2772
+ | Predictive query | `tactics/predictiveQuery` | `predictAreaEffect`, `predictArcEffect`, `predictTiles`, `PredictiveDeps`, `PredictedTarget` |
2773
+ | Sim snapshot | `tactics/snapshot` | `createSnapshotStore`, `SnapshotStore`, `SnapshotSlice`, `Snapshot`, `deepClone` |
2774
+ | Surfaces | `tactics/surface` | `createSurfaceLayer`, `SurfaceLayer`, `SurfaceLayerConfig`, `SurfaceKindDef`, `SurfaceReaction`, `SurfaceEvent` |
2775
+ | Area targeting | `combat/effects` | `resolveAreaTargets`, `AreaTarget`, `AreaTargetInput` (shared AoE targeting behind `effect` + the predictive query) |
2776
+ | Environment field | `world/envField` | `createEnvironmentField`, `EnvironmentField`, `EnvironmentSample`, `EnvironmentFieldConfig`, `OccluderRect`, `HeatSource` |
2777
+ | Weather + fire | `world/weather` | `resolveWeather`, `WeatherState`, `WeatherModifier`, `WeatherModifierTable`, `ResolvedWeather`, `createFireGrid`, `FireGrid`, `FireCell`, `FireGridConfig` |
2778
+ | Realm composition | `world/realm` | `composeRealm`, `RealmCard`, `RealmBase`, `ComposedRealm`, `RealmEnvironmentParams`, `SpawnTableOverride` |
2779
+ | Decay meters | `survival/decayMeter` | `createDecayMeterSet`, `DecayMeterSet`, `DecayMeterConfig`, `MeterThreshold`, `DecayMeterState` |
2780
+ | Status moodles | `survival/moodle` | `createMoodleStack`, `stackMoodles`, `MoodleStack`, `Moodle`, `MoodleSeverity`, `TimedMoodleInput` |
2781
+ | Multi-region health | `survival/regionHealth` | `createMultiRegionHealth`, `MultiRegionHealth`, `HealthRegionConfig`, `AilmentConfig`, `RegionHealthState`, `AilmentInstance` |
2782
+ | Audio contract | `audio/audioFalloff` | `computeFalloffGain`, `resolveEmitterGain`, `distance3`, `AudioFalloffConfig`, `FalloffCurve`, `SoundDef`, `AudioBusDef`, `AudioBusId` |
2783
+ | Beat clock | `time/beatClock` | `createBeatClock`, `createBeatInputBuffer`, `nextBeatTime`, `BeatClock`, `BeatClockConfig`, `BeatSnapshot`, `BeatInputBuffer`, `BufferedAction` |
2784
+ | Spawn director | `ai/spawnDirector` | `createSpawnDirectorState`, `advanceSpawnDirector`, `advanceWave`, `raiseAlert`, `pickSpawnPoint`, `SpawnDirectorConfig`, `WaveManifest`, `SpawnEntry`, `SpawnRequest`, `DirectorContext` |
2785
+ | Threat table | `ai/threat` | `createThreatTable`, `ThreatTable`, `ThreatTableConfig`, `ThreatEntry`, `HighestThreatOptions` |
2786
+ | Job board | `ai/jobBoard` | `createJobBoard`, `JobBoard`, `JobDef`, `Job`, `JobPhase`, `WorkerState`, `JobReport`, `JobTickContext` |
2787
+ | Crowd flow | `ai/crowd` | `computeFlowField`, `createCrowdField`, `selectPoi`, `FlowField`, `FlowFieldOptions`, `CrowdField`, `Poi`, `SelectPoiOptions` |
2788
+ | Physics actors | `physics/ragdoll`, `physics/carryable`, `physics/forceVolume`, `physics/spatialGrid` | `createRagdoll`, `Ragdoll`, `Carryable`, `carrySpeedMultiplier`, `ForceVolume`, `PlatformCarry`, `SpatialGrid` |
2789
+ | Traversal (grapple/glide) | `physics/traversal` | `Grapple`, `GrappleConfig`, `Glide`, `GlideConfig` |
2790
+ | Structural destruction | `physics/structure` | `StructureGraph`, `StructureNodeSpec`, `StructureEdgeSpec`, `StructureMaterial`, `StructureMaterialTable`, `CollapseEvent`, `DebrisConfig` |
2791
+ | Destructible terrain | `world/carve` | `VoxelVolume`, `VoxelMaterial`, `VoxelMaterialTable`, `CarvableField`, `carvableTerrain`, `CarveOp`, `DepositOp`, `CraterOp`, `MoundOp`, `EMPTY_VOXEL` |
2792
+ | Vehicle body | `physics/vehicleBody` | `createVehicleBody`, `VehicleBody`, `VehicleBodyConfig`, `WheelSpec`, `GripCurve`, `sampleGripCurve`, `DEFAULT_GRIP_CURVE` |
2793
+ | Buoyant boat | `physics/buoyancy` | `createBuoyantBody`, `BuoyantBody`, `BuoyantBodyConfig` |
2794
+ | Crash damage | `physics/damageZones` | `createDamageModel`, `DamageModel`, `DamageZoneDef`, `DamageTransition` |
2795
+ | Mounts / rideables | `scene/mount` | `createMountController`, `MountController`, `MountKit`, `MountSeat`, `RideableConfig` |
2796
+ | Shared-vehicle stations | `scene/stationClaim` | `createStationClaim`, `StationClaim`, `Station`, `SharedVehicleConfig`, `ClaimResult` |
2797
+ | Lag compensation | `multiplayer/lagCompensation` | `createPositionHistory`, `PositionHistory`, `rewindTimestamp`, `resolveHitscan`, `raySphereDistance`, `HitscanRay`, `HitscanTarget` |
2798
+ | Simultaneous commit | `multiplayer/simultaneousCommit` | `createCommitRound`, `CommitRound`, `SealedCommit`, `resolveCommits` |
2799
+ | Combat-snapshot replay | `multiplayer/combatSnapshot` | `serializeBoard`, `cloneSnapshot`, `replayCombat`, `BoardSnapshot`, `SnapshotUnit`, `CombatRules`, `ReplayResult` |
2800
+ | Session matchmaking | `multiplayer/matchmaking` | `browseSessions`, `findByJoinCode`, `quickMatch`, `matchesFilter`, `normalizeJoinCode`, `generateJoinCode`, `SessionListing`, `MatchFilter` |
2801
+ | Auth identity | `multiplayer/identity` | `AuthSession`, `PlayerIdentity`, `sessionPlayer`, `resolveGuestSession` |
2802
+ | Text chat | `game/chat`, `multiplayer/chatContract` | `createChat`, `Chat`, `ChatMessage`, `ChatChannelDef`, `whisperChannelId`, `createChatRateLimiter`, `ChatTransport`, `ChatSync`, `createLocalChatTransport` |
2803
+ | Voice seam | `multiplayer/voiceContract` | `VoiceTransport`, `VoiceParticipant`, `VoiceRoute`, `createLocalVoiceTransport`, `createPushToTalk`, `PushToTalkMode` |
2804
+ | Race state | `game/race` | `raceTrack`, `RaceTrack`, `createRaceState`, `RaceState`, `RaceEvent`, `RaceWinCondition`, `firstPastPost`, `topK`, `lastStanding`, `everyoneFinishes` |
2805
+ | Reveal query | `sensor/revealQuery` | `createRevealQuery`, `RevealQuery`, `RevealQueryOptions`, `RevealHit` |
2806
+ | Hidden-state probe | `sensor/hiddenStateProbe` | `probeHiddenState`, `probeHiddenStateAll`, `HiddenStateSource`, `HiddenStateValue`, `SensorProbeOptions`, `SensorReading` |
2807
+ | View-frustum sensor | `sensor/frustumSensor` | `createFrustumSensor`, `projectToView`, `framingScore`, `FrustumCamera`, `FrustumTarget`, `FrustumProjection`, `FrustumSample`, `FrustumSensor`, `FramingConfig` |
2808
+ | Recording buffer | `sensor/recordingBuffer` | `createRecordingBuffer`, `RecordingBuffer`, `RecordingFrame`, `RecordingBufferOptions` |
2809
+ | Concealment scoring | `sensor/concealment` | `colorDistance`, `concealmentScore`, `createConcealmentSensor`, `ColorHex`, `ConcealmentTarget`, `ConcealmentSample`, `ConcealmentSensor` |
2810
+ | Freeze violation monitor | `sensor/freezeMonitor` | `createFreezeMonitor`, `FreezeMonitor`, `FreezeSubject`, `FreezeViolation` |
2811
+ | Animation SM | `combat/animationState` | `createAnimationState`, `AnimationState`, `AnimationClip`, `FramePhase`, `FrameRange`, `phasesAtFrame`, `activeRangeAtFrame`, `frameAtMs` |
2812
+ | Accumulator meter | `stats/accumulatorMeter` | `createAccumulatorMeter`, `AccumulatorMeter`, `AccumulatorMeterConfig`, `MeterTier`, `MeterAddResult`, `tierAt` |
2813
+ | Stagger / buildup | `combat/breakMeters` | `createStaggerMeter`, `createBuildupMeter`, `StaggerMeter`, `BuildupMeter`, `BuildupProc` |
2814
+ | Attack tags | `combat/attackTags` | `attackMeta`, `AttackTag`, `AttackMeta`, `hasTag`, `isBlockable`, `isParryable`, `isDodgeable`, `counters` |
2815
+ | Defensive window | `combat/defensiveWindow` | `createDefensiveWindow`, `resolveDefense`, `DefensiveWindowConfig`, `DefenseKind`, `DefenseOutcome`, `windowActiveAt`, `iframeActiveAt` |
2816
+ | Combo string | `combat/comboString` | `createComboRunner`, `advanceCombo`, `ComboString`, `ComboStep`, `AdvanceComboResult` |
2817
+ | Hit reaction | `combat/hitReaction` | `resolveHitReaction`, `HitReaction`, `HitReactionConfig`, `CameraShake`, `applyImpulse` |
2818
+ | Telegraph | `combat/telegraph` | `pointInTelegraph`, `telegraphProgress`, `telegraphFired`, `telegraphTurnProgress`, `telegraphFiredAtTurn`, `telegraphTurnsRemaining`, `TelegraphShape`, `TelegraphConfig` |
2819
+ | Dash / dodge | `movement/dash` | `createDashState`, `DashState`, `DashConfig`, `DashBurst`, `iframeActive`, `dashOffset` |
2820
+ | Ability kit | `combat/abilityKit` | `createAbilityKit`, `AbilityKit`, `AbilitySlotConfig`, `AbilitySlotSnapshot`, `AbilitySlotState`, `AbilityCastType`, `AbilityCastResult`, `AbilitySlotRetune` |
2821
+ | Event meter | `stats/eventMeter` | `createEventMeter`, `EventMeter`, `EventMeterConfig`, `EventMeterFeedResult` |
2822
+ | Auto-target policy | `scene/autoTarget` | `selectAutoTarget`, `createAutoTargeter`, `AutoTargetPolicy`, `AutoTargeter`, `AutoTargetDeps` |
2823
+ | Resistance matrix | `combat/resistance` | `resolveResistance`, `resistanceScale`, `ResistanceMatrix`, `ResistVerdict`, `ResistanceResult` |
2824
+ | Run draft | `game/runDraft` | `createRunDraft`, `createRunModifierStack`, `RunDraft`, `RunModifierStack`, `RunModifierOffer` |
2825
+ | Uniform-cell grid | `puzzle/cellGrid` | `CellGrid`, `CellRun`, `createCellGrid`, `cellAt`, `inGridBounds`, `withCell`, `withCells`, `fullRows`, `clearRows`, `collapseColumns`, `findRuns` |
2826
+ | Falling piece | `puzzle/fallingPiece` | `FallingPiece`, `ShapeTable`, `LockDelayState`, `pieceCells`, `pieceCollides`, `mergePiece`, `dropDistance`, `gravityInterval`, `levelForLines`, `lineScore`, `createLockDelay`, `stepLockDelay` |
2827
+ | Falling tile grid | `tactics/fallingGrid` | `createFallingGrid`, `FallingGrid`, `FallingGridConfig`, `FallingGridCell`, `FallingGridSnapshot`, `LockState`, `gravityIntervalMs`, `GravityIntervalConfig` — a generic tile-drop grid (any `TCell` payload), distinct from `puzzle/cellGrid`+`puzzle/fallingPiece`'s row-clear/shape-table pair |
2828
+ | Spawn/respawn points | `game/spawnPoints` | `createSpawnPoints`, `SpawnPoints`, `SpawnPointPose`, `RespawnTarget` |
2829
+ | Level sequence | `game/levelSequence` | `createLevelSequence`, `LevelSequence`, `LevelSequenceConfig`, `LevelDescriptor`, `CurrentLevel`, `LevelSequenceStatus`, `LevelSequenceProgress` |
2830
+ | Devtools overlay + tunables | `devtools/devtools` | `devtools`, `createDevtools`, `tunable`, `snapshotDevtools`, `instrumentLatency`, `Tunable`, `TunableOptions`, `TunableAccessor`, `DevtoolsControl`, `DiscoveredEntry`, `DevtoolsOverrides`, `DevtoolsSnapshot` |
2831
+ | Tunable auto-discovery | `devtools/transformTunables` | `transformTunableExports`, `tunableModuleTable`, `tunableDiscoveryPlugin`, `TunableTransformResult` |
2832
+
2833
+ ## Getting started (new project)
2834
+
2835
+ ```sh
2836
+ bun add @jgengine/core @jgengine/react @jgengine/shell react react-dom three three-stdlib @react-three/fiber @react-three/drei
2837
+ bun add -d @tailwindcss/vite tailwindcss # HUD styling (Vite + Tailwind v4)
2838
+ ```
2839
+
2840
+ A single game's standalone entry mounts `GameHost` (`@jgengine/shell/GameHost`) over the `game` your `game.config.ts` exports. The full standalone harness is four small files plus a script — this is exactly the shape every `Games/*` game ships, so `bun dev` plays it on its own with no host app:
2841
+
2842
+ ```html
2843
+ <!-- index.html -->
2844
+ <!doctype html>
2845
+ <html lang="en">
2846
+ <head>
2847
+ <meta charset="UTF-8" />
2848
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2849
+ <title>My Game</title>
2850
+ </head>
2851
+ <body>
2852
+ <div id="root"></div>
2853
+ <script type="module" src="/src/main.tsx"></script>
2854
+ </body>
2855
+ </html>
2856
+ ```
2857
+
2858
+ ```ts
2859
+ // vite.config.ts — monorepo-aware: the alias branch only fires when this folder
2860
+ // sits inside the engine repo checkout; copied anywhere else, @jgengine/* resolves
2861
+ // from npm dist and the alias list is empty
2862
+ import { existsSync } from "node:fs";
2863
+ import { fileURLToPath } from "node:url";
2864
+ import tailwindcss from "@tailwindcss/vite";
2865
+ import react from "@vitejs/plugin-react";
2866
+ import { defineConfig } from "vite";
2867
+
2868
+ const engineSrc = (pkg: string) => fileURLToPath(new URL(`../../packages/${pkg}/src`, import.meta.url));
2869
+
2870
+ export default defineConfig({
2871
+ plugins: [react(), tailwindcss()],
2872
+ resolve: {
2873
+ alias: existsSync(engineSrc("core"))
2874
+ ? [
2875
+ { find: /^@jgengine\/core\/(.*)$/, replacement: `${engineSrc("core")}/$1` },
2876
+ { find: /^@jgengine\/react\/(.*)$/, replacement: `${engineSrc("react")}/$1` },
2877
+ { find: /^@jgengine\/ws\/(.*)$/, replacement: `${engineSrc("ws")}/$1` },
2878
+ { find: /^@jgengine\/shell\/(.*)$/, replacement: `${engineSrc("shell")}/$1` },
2879
+ { find: /^@jgengine\/assets$/, replacement: `${engineSrc("assets")}/index.ts` },
2880
+ { find: /^@jgengine\/assets\/(.*)$/, replacement: `${engineSrc("assets")}/$1` },
2881
+ ]
2882
+ : [],
2883
+ },
2884
+ });
2885
+ ```
2886
+
2887
+ ```css
2888
+ /* src/index.css */
2889
+ @import "tailwindcss";
2890
+ @source "../node_modules/@jgengine/react/dist";
2891
+ @source "../node_modules/@jgengine/shell/dist";
2892
+ ```
2893
+
2894
+ Inside the engine repo the two `@source` lines point at `../../../packages/react/src` and `../../../packages/shell/src` instead (see any `Games/*/src/index.css`) — same file, different `@source` targets depending on where dist lives.
2895
+
2896
+ ```tsx
2897
+ // main.tsx
2898
+ import "./index.css";
2899
+
2900
+ import { createRoot } from "react-dom/client";
2901
+ import { GameHost } from "@jgengine/shell/GameHost";
2902
+ import { game } from "./game.config";
2903
+
2904
+ const root = document.getElementById("root");
2905
+ if (root === null) throw new Error("main: missing #root mount element");
2906
+ createRoot(root).render(<GameHost playable={game} />);
2907
+ ```
2908
+
2909
+ Add `"dev": "vite"` to `package.json`'s `scripts` — `bun dev` then launches the game standalone.
2910
+
2911
+ `GameHost` resolves multiplayer itself from `game.multiplayer` (falling back to offline when the adapter can't resolve, with a console warning) — pass `multiplayer` (a prebuilt `ShellMultiplayer | null`, used as-is with no resolution attempted) or `resolveMultiplayer` (`(args) => ShellMultiplayer | null`, tried before the built-in resolver, falling back to it on `null`) only when the host app needs to supply its own session, e.g. trying several transports in sequence.
2912
+
2913
+ A multi-game host (a launcher, a dev registry) wires `GamePlayer` over a `GameRegistry`:
2914
+
2915
+ ```tsx
2916
+ import { GamePlayer } from "@jgengine/shell/GamePlayer";
2917
+ import type { GameRegistry } from "@jgengine/shell/registry";
2918
+
2919
+ const games: GameRegistry = {
2920
+ "my-game": () => import("./my-game").then((m) => m.game),
2921
+ };
2922
+
2923
+ function App() {
2924
+ return <GamePlayer gameId="my-game" registry={games} loading={<p>Loading…</p>} />;
2925
+ }
2926
+ ```
2927
+
2928
+ `GamePlayer({ gameId, registry, fallbackGameId?, loading?, multiplayer? })` (`@jgengine/shell/GamePlayer`) is `GamePlayerShell` plus the lazy-load glue: it looks up `gameId` in `registry`, awaits the loader, renders `loading` (default `null`) until it resolves, then mounts `GamePlayerShell playable={...} multiplayer={...}`; switching `gameId` re-triggers the load, and an in-flight load is discarded if the id changes again first. `resolveGameLoader(registry, gameId, fallbackGameId?)` (`@jgengine/shell/registry`) is the underlying lookup — `registry[gameId] ?? registry[fallbackGameId]` — for hosts that want the fallback behavior without the component.
2929
+
2930
+ HUD styling is Tailwind v4 via the `index.css` above — without its `@source` lines the HUD renders unstyled. Then build the game itself under `src/` per the layout below — `src/game.config.ts` is the single entry, defined with `defineGame` from `@jgengine/shell/defineGame`.
2931
+
2932
+ ## Scope
2933
+
2934
+ This file documents engine primitives and conventions only — never game domain. Example ids (`iron_block`, `mob_grunt`, `shop_town`) are placeholders, not content to copy.
2935
+
2936
+ | Engine owns | Your game owns |
2937
+ |-------------|----------------|
2938
+ | Weighted loot RNG, trade validation, loadout application, quest journal state, social graph, stat clamp math, effect absorption, projectile geometry, death resolution, event bus, feeds, leaderboards, input capture, pose hitboxes | Catalog entries and ids, effect id names, XP curves, shop/item/quest/loadout definitions, use-handlers, AI logic, UI content |
2939
+
2940
+ **Rules:**
2941
+
2942
+ 1. **Catalog-first** — shape and behavior of every id lives in game-owned catalog files. Runtime calls pass ids, positions, instance keys.
2943
+ 2. **Three buckets** — inventory items, scene objects, scene entities. Never merge them.
2944
+ 3. **Dumb place/spawn** — no behaviors on `place()`/`spawn()`; the catalog owns them.
2945
+ 4. **Commands for verbs** — input maps to actions, actions to commands/handlers; no raw keys in game logic.
2946
+ 5. **Primitives over glue** — a loop several games need (loot roll, shop buy, kit seeding) belongs in the engine, not copy-pasted per game.
2947
+ 6. **No speculative config** — `defineGame` fields exist only with a live engine consumer.
2948
+ 7. **This file stays domain-free.**
2949
+
2950
+ ## The three buckets
2951
+
2952
+ | Bucket | What | API |
2953
+ |--------|------|-----|
2954
+ | **Inventory** | Stackable ids in containers | `ctx.player.inventory.put / take / move / has / count` |
2955
+ | **Scene object** | Static world content | `ctx.scene.object.place / remove / move / rotate / list` |
2956
+ | **Scene entity** | Movers driven per tick | `ctx.scene.entity.spawn / despawn / setPose / effect / …` |
2957
+
2958
+ A voxel block is an object. A rack is an object with a slot inventory. A GPU is an inventory item inside it. A player, mob, or car is an entity. A dropped-item lying on the ground is also an entity — `ctx.scene.worldItem` (position + item ref + rarity, spawned under `game/worldItem`'s `WORLD_ITEM_ENTITY_NAME`) — never a fourth bucket and never merged into inventory or object.
2959
+
2960
+ ## Game repo layout
2961
+
2962
+ Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. Dense files — one `catalog.ts` per domain, never one file per entry.
2963
+
2964
+ ```
2965
+ src/
2966
+ game.config.ts single entry — export const game = defineGame({...}) from "@jgengine/shell/defineGame"
2967
+ index.tsx barrel — export { game } from "./game.config" (+ any UI-preview scenario re-export)
2968
+ main.tsx standalone host — mounts <GameHost playable={game}/> from "@jgengine/shell/GameHost"
2969
+ loop.ts onInit, onNewPlayer, onTick
2970
+ world.ts WorldFeature + PhysicsConfig (only for games that have one)
2971
+ game/
2972
+ keybinds.ts ActionCodesMap — named actions + hotbarSlotBindings(n)
2973
+ inventories.ts inventory declarations
2974
+ assets.ts Render catalog
2975
+ content.ts itemById / entityById lookups over all catalogs
2976
+ loadouts.ts Loadout ids → items/economy/unlocks per inventory
2977
+ world/ zones.ts, setup.ts (place/spawn from onInit)
2978
+ items/ <domain>/catalog.ts + use-handlers.ts
2979
+ objects/ catalog.ts (+ loot tables beside their domain)
2980
+ entities/ players/ enemies/ npcs/ — catalog.ts per role (never actors/)
2981
+ quests/catalog.ts when using game.quest
2982
+ progression/ curves.ts — game-owned XP curve numbers fed to game/progression
2983
+ ui/GameUI.tsx ALL layout/positioning
2984
+ ui/components/ content-only pieces GameUI places
2985
+ ```
2986
+
2987
+ ## `defineGame` — the single authoring entry
2988
+
2989
+ `@jgengine/shell/defineGame` is the game-authoring entry: one call in `game.config.ts` takes both engine fields (`name`, `assets`, `world`, `physics`, `inventories`, `input`, `server`, `save`, `time`, `feed`, `multiplayer`) and presentation fields (`content`, `loop`, `GameUI`, `camera`, `environment`, `WorldOverlay`, `renderEntity`, `renderObject`, `entitySprites`, `entityModels`, `objectModels`, `hotbarSelection`, `prompts`, `pointer`, `touch`, `worldHealthBars`, `audio`, `entitySounds`, `objectSounds`, `worldItem`, `shadows`, `collision`, `movement`, `devtools`) and returns a ready `PlayableGame` — no separate object to assemble. It is a thin wrapper over the core `defineGame` primitive (below) plus the `PlayableGame` runner assembly; see `packages/shell/src/defineGame.tsx` for the exact accepted fields. Never game tuning (walk speeds, damage, prompts — those live in catalogs).
2990
+
2991
+ **Smart defaults** — omit any of these and the call still resolves: `multiplayer` → `offline()`; `assets` → an empty asset catalog; `loop` hooks (`onInit`/`onNewPlayer`/`onTick`) → no-ops; `content` → `{}`; `GameUI` → an empty component; `camera` → third-person orbit; `feed` → 20-entry ring buffers per action; a `world` of kind `environment()` auto-renders as the backdrop with no `environment` component supplied — a non-`environment()` world (`flat()`, `voxel()`, …) still needs the game to hand it one.
2992
+
2993
+ ```ts
2994
+ // game.config.ts — imports only, nothing inline
2995
+ import { defineGame } from "@jgengine/shell/defineGame";
2996
+ import { assets } from "./game/assets";
2997
+ import { content } from "./game/content";
2998
+ import { GameUI } from "./game/ui/GameUI";
2999
+ import { inventories } from "./game/inventories";
3000
+ import { keybinds } from "./game/keybinds";
3001
+ import { loop } from "./loop";
3002
+ import { physics, world } from "./world";
3003
+
3004
+ export const game = defineGame({
3005
+ name: "My Game",
3006
+ assets,
3007
+ world,
3008
+ physics,
3009
+ inventories,
3010
+ input: keybinds,
3011
+ server: "persistent", // or { mode: "ffa", scoreLimit: 30 } — rules live in game code
3012
+ save: { auto: "5m", scope: "player+chunks" }, // or "none"
3013
+ multiplayer: offline(), // or ws({ topology, url? }) / fly({ app }) / convex({ topology }) / socketIo({ topology, url? }) / p2p({ room? }) / lan({ port?, path? }) / servers({ …, adapter }) — defaults to offline()
3014
+ content,
3015
+ loop, // Partial<GameLoop<GameContext>> — missing hooks default to no-ops
3016
+ GameUI,
3017
+ camera: { perspective: "third" }, // optional — this is the default
3018
+ });
3019
+ ```
3020
+
3021
+ ```ts
3022
+ // game/keybinds.ts — named actions + generated hotbar slots; one key, one action
3023
+ import { hotbarSlotBindings, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
3024
+
3025
+ export const keybinds: ActionCodesMap = {
3026
+ moveForward: ["KeyW"], moveBack: ["KeyS"], moveLeft: ["KeyA"], moveRight: ["KeyD"],
3027
+ jump: ["Space"], sprint: ["ShiftLeft"],
3028
+ interact: ["KeyE"],
3029
+ crouch: { hold: ["KeyC"], toggle: ["KeyZ"] },
3030
+ aim: { hold: ["mouse2"], toggle: ["KeyV"] },
3031
+ tabTarget: ["Tab"], clearTarget: ["Escape"],
3032
+ ...hotbarSlotBindings(9), // hotbarSlot1..9 → Digit1..9 (a 10th slot gets Digit0)
3033
+ };
3034
+ ```
3035
+
3036
+ ```ts
3037
+ // game/inventories.ts
3038
+ import type { InventoryDeclaration } from "@jgengine/core/game/defineGame";
3039
+ export const inventories: Record<string, InventoryDeclaration> = {
3040
+ hotbar: { slots: 9, hud: "hotbar" },
3041
+ backpack: { slots: 28, traits: itemTraits },
3042
+ equipment: { slots: 4, accepts: ["weapon", "armor"], applyModifiers: true },
3043
+ };
3044
+
3045
+ // world.ts — top of src/, not under game/
3046
+ import type { PhysicsConfig } from "@jgengine/core/game/defineGame";
3047
+ import { biomes, type WorldFeature } from "@jgengine/core/world/features";
3048
+ export const world: WorldFeature = biomes({ map: "world/biomes", zones: "world/zones" });
3049
+ export const physics: PhysicsConfig = { gravity: -32 };
3050
+ ```
3051
+
3052
+ - `PhysicsConfig.gravity`/`jumpVelocity` tune the shell's built-in walk controller's fall/jump feel (defaults ~`-24`/`7.1`) — the only two levers on gravity and jump height; everything else about movement (speed, poses) stays catalog `movement` fields.
3053
+ - Input bindings are string arrays (hold semantics) or `{ hold, toggle, repeatMs? }` for the same verb. `repeatMs` turns a held action into an auto-repeat fire (build-mode place-on-drag, rapid-fire without a separate `wasPressed`/interval combo in game code) — the shell refires the command every `repeatMs` while the binding stays down, on top of its normal press-edge fire.
3054
+ - **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback).
3055
+ - **`interact`** is special: pressing it resolves the active proximity prompt from the `prompts` field of `defineGame({...})` and runs that prompt's `invoke` command. A prompt with `invoke: null` is display-only and does nothing on the key.
3056
+ - UI keybind badges derive from `keybinds` via `actionLabel(keybinds, "openBackpack")` — `bindingLabel` maps codes to short labels (`Digit1`→`1`, `KeyB`→`B`, `mouse0`→`LMB`, `Escape`→`Esc`). Never hardcode label strings; they drift the moment a binding changes.
3057
+ - `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
3058
+ - Never in defineGame: player tuning, catalog helpers (`defineItems` etc.), game nouns, behaviors, prompts, or inline binding/inventory/world blobs. The one exception is `physics.gravity`/`physics.jumpVelocity` — global controller tuning, not a catalog value (see "Controller kinematics" below).
3059
+ - `assets` may be omitted for a game with no models (a HUD-only card/board game, say) — `defineGame` injects an empty catalog, so `GameDefinition.assets` is always present downstream with no per-caller `?.` checks.
3060
+ - `devtools` defaults to `true` — every game gets the F2-toggled debug overlay (Perf/Tune/Logs/Net/Keys) for free, and every top-level `export const` number/boolean/color and every exported flat table of them under `src/` is auto-discovered into the Tune tab with zero game code; set `false` to disable the toggle entirely. See "Devtools — F2 overlay and tunables" below.
3061
+
3062
+ ### `@jgengine/core/game/defineGame` — the underlying primitive
3063
+
3064
+ The low-level engine boot call the shell `defineGame` composes internally: engine fields only (`name`, `assets`, `world`, `physics`, `inventories`, `input`, `server`, `save`, `time`, `feed`, `multiplayer`, `loop`) — no `content`/`GameUI`/`camera`/render fields, those are the shell layer's job.
3065
+
3066
+ ```ts
3067
+ import { defineGame as defineEngineGame } from "@jgengine/core/game/defineGame";
3068
+ import { offline } from "@jgengine/core/runtime/adapter";
3069
+
3070
+ const game = defineEngineGame({
3071
+ name: "My Game",
3072
+ assets, world, physics, inventories,
3073
+ input: keybinds,
3074
+ server: "persistent",
3075
+ save: { auto: "5m", scope: "player+chunks" },
3076
+ multiplayer: offline(),
3077
+ loop, // GameLoop<GameContext>
3078
+ });
3079
+ ```
3080
+
3081
+ Reach for this directly only outside a React host — a headless server, a non-shell runner; a browser game authors through `@jgengine/shell/defineGame` above, which calls this and returns the `PlayableGame` a runner needs.
3082
+
3083
+ ## `PlayableGame` — how a game plugs into a runner
3084
+
3085
+ The type `@jgengine/shell/defineGame` returns and every runner (`GameHost`, `GamePlayerShell`) consumes. A game never builds this object by hand — `defineGame({...})` assembles it from the merged config. Source type at `@jgengine/core/game/playableGame`:
3086
+
3087
+ ```ts
3088
+ export type PlayableGame<TUi = unknown> = {
3089
+ game: GameDefinition;
3090
+ content: GameContextContent; // { itemById?, entityById?, objectById? }
3091
+ loop: Required<GameLoop<GameContext>>; // onInit, onNewPlayer, onTick
3092
+ GameUI: TUi; // React component in web runners
3093
+ prompts?: (ctx: GameContext) => readonly PositionedPrompt[]; // interact-key + HUD source
3094
+ };
3095
+ ```
3096
+
3097
+ `prompts` is the **single source** of positioned proximity prompts: the shell reads it to fire the `interact` key, and the HUD should read the same list through `useActivePrompt(playable.prompts?.(ctx))` rather than building its own — one list, no drift. A prompt is only actionable if its `invoke` is non-null.
3098
+
3099
+ Optional render/world fields the shell also reads: `entitySprites` / `entityModels` (billboards / GLBs keyed by entity kind), `objectModels` (GLBs keyed by object catalog id), `renderObject` (per-object visual override — return your own mesh for a placed object and the shell still positions it; falls back to `objectModels` → colored box), `WorldOverlay` (canvas-layer VFX), `environment` (canvas-layer scenery — ground/sky/structures; when set, replaces the default ground plane + debug grid + rock field), `camera`, `shadows` (cast/receive shadows across the R3F canvas; default true), and `worldHealthBars` (`boolean | { statId?, roles? }` — `roles` restricts bars to entities whose catalog `role` is in the given `CatalogEntityRole` list, e.g. skip friendly NPCs). A model value is a catalog id (`string`, resolved via `game.assets`) or an inline `ModelConfig { url, scale?, y?, anchor?, dims?, material?, animation? }` (`material` overrides color/metalness/roughness/emissive/emissiveIntensity on the cloned mesh, leaving shared GLTF caches untouched; `animation?: { clip?, loop?, timeScale?, paused?, time? }` plays a GLTF clip — `clip` defaults to the first clip, `loop` defaults true — and `paused: true` + `time: <seconds>` holds the rig on one fixed frame, a pose library for inventory previews or held cutscene poses). Catalog-resolved models carry measured `dims` (`catalog.resolve(id).dims = { footprint:{w,d}, center:{x,z}, minY }`); with the default `anchor: "center"` the shell centers the footprint on the placement point and ground-snaps `minY` to it, so corner-pivot kit models place correctly with no per-game pivot math. Applies through both `entityModels` and `objectModels`.
3100
+
3101
+ `renderEntity?: (entity: SceneEntity) => ReactNode` and `renderObject?: (object: SceneObject) => ReactNode` hand you the mesh for one entity/object while the shell still positions it and keeps it tagged for picking/selection; return null/undefined to fall through to model → sprite/box. `objectStyles?: Record<catalogId, { color?, opacity?, hidden? }>` styles the default colored-box object render — `color` overrides the hash color, `opacity < 1` sets transparent, `hidden` skips the mesh but keeps the picking tag.
3102
+
3103
+ **Presentation mode.** `PlayableGame.presentation`: `"3d"` (default) mounts the canvas, camera rig, and pointer; `"hud"` mounts none of that — the game is `GameUI` plus the command/input loop, for board/card/menu games that need no 3D camera at all.
3104
+
3105
+ **Auto environment.** When `world` is an `environment()` descriptor and `PlayableGame.environment` is unset, the shell renders that descriptor as the backdrop automatically — no manual `environment` wiring needed for the common case. Set `environment` explicitly only to override that default (a custom canvas component always wins). The same auto-render convention covers grid-cell worlds (`biomes`/`voxel`/`plots`/`tilemap`) — see "World features" below.
3106
+
3107
+ **Lighting and backdrop.** `PlayableGame.lighting` (`LightingConfig`, `@jgengine/core/game/playableGame`) replaces the shell's hardcoded ambient/directional default when present, regardless of world kind: `ambient?: { color?, intensity? }`, `directional?: { color?, intensity?, position, castShadow? }[]`, `hemisphere?: { skyColor?, groundColor?, intensity? }`. `PlayableGame.backdrop` (`BackdropConfig`) is a generic background/sky/fog for **any** world kind, including a custom `environment` component: `background?: string` (CSS color), `sky?: SkyEnvironmentConfig` (same descriptor `environment()`'s `sky` field takes), `fog?: { color?, near?, far?, density? }` (`density` set switches to exponential `FogExp2` and `near`/`far` are ignored). Both are optional and additive to whatever the world/`environment` renderer already draws.
3108
+
3109
+ **Player movement tuning** — the `movement` field (`PlayerMovementConfig`) tunes the shell's local-player walk controller beyond `physics.gravity`/`jumpVelocity`: `mode` (`"free"` camera-relative default, `"axis"` locks travel to one world `axis`, `"grid"` snaps each committed step to `cellSize` centers), `collideObjects` (collide against placed scene objects as unit-box AABBs even without `collision.voxel`), and `beforeCommit(frame)` — an escape hatch called each frame with `{ entityId, current, next, dt }` that can return a replacement `[x, y, z]` to constrain or redirect the step (rails, bounds, custom collision) before it commits and before `onTick` runs.
3110
+
3111
+ The runner boots `createGameContext({ definition, content, player: { userId, isNew } })`, calls `loop.onInit(ctx)` then `loop.onNewPlayer(ctx)`, and drives `loop.onTick(ctx, dt)` per frame. **Convention: `onNewPlayer` spawns the player entity with `id === ctx.player.userId`** — bounded stats, targeting, and kill attribution key off that.
3112
+
3113
+ ### Object spatial queries, entity patching, and surface sampling
3114
+
3115
+ ```ts
3116
+ ctx.scene.object.at(x, y, z) // cell lookup, cell size 1, most-recent wins
3117
+ ctx.scene.object.inBox(min, max) // inclusive AABB query
3118
+ ctx.scene.object.raycast({ origin, direction, maxDistance, halfExtents?, filter? }) // → nearest hit or null
3119
+ ctx.scene.object.raycastAll({ origin, direction, maxDistance, halfExtents?, filter? }) // → hits, nearest-first
3120
+ ctx.scene.entity.update(id, patch) // name/position/rotations/role/movement/behaviors/meta; false for unknown id
3121
+ ```
3122
+
3123
+ Placed objects are unit boxes (half-extents `[0.5, 0.5, 0.5]`) centered on position, matching the shell's default render, so `raycast`/`raycastAll` (`scene/objectQuery`) match what a player sees. `entity.update` notifies subscribers and bumps `ctx.version()` the same as `spawn`/`despawn`/`setPose` — it's the general-purpose patch the more specific methods (`setPose`, `form.shapeshift`, `possession.possess`) build on.
3124
+
3125
+ `ctx.scene.object.place(catalogId, x, y, z, { instanceId?, parentSpace?, rotation?, visual? })` takes an optional `visual: ObjectVisual` (`@jgengine/core/scene/objectStore`) — `{ scale?: number | [x,y,z], color?, opacity? }` — a per-instance render override independent of the catalog entry; `ctx.scene.object.setVisual(instanceId, visual | undefined)` changes it after placement (`undefined` clears back to the catalog default). Distinct from `objectStyles` on `PlayableGame` (styles a catalog id for every instance); `visual`/`setVisual` targets one placed instance (a damaged crate, a dyed banner, a resized prop).
3126
+
3127
+ `pointerService.worldHitCenter()` (shell) casts from the viewport center regardless of cursor presence (pointer-lock aim) — combine with `pointer.worldHit()` (cursor-driven) to support both mouse-look and free-cursor games from the same probe. `PointerHit` also carries an optional `uv?: { u, v }` on UV-mapped mesh hits (absent for the ground fallback), `material?: { color, metalness?, roughness? } | null` sampled off the hit mesh's `MeshStandardMaterial` (`null`/unset for non-standard materials, e.g. the ground plane) — combine `uv` + `material` for paint tools, decals, and material-aware interaction — and `instanceId?: number`, the hit index when the intersected mesh is a `THREE.InstancedMesh` (grid-world cells, `InstancedBodies` debris, any instanced render), absent otherwise.
3128
+
3129
+ **Runtime paint layer** (`ctx.scene.entity.paint`, backed by `scene/paintLayer`) — a `PaintLayer` keyed by instance id (entity or object): `paint(instanceId, { u, v, radius, color })`, `strokes(instanceId)`, `clear(instanceId?)`, `version(instanceId)` (bumps per paint/clear), `subscribe(listener)`. The shell auto-renders painted instances through a lazily-created 512×512 canvas texture kept in sync — no per-game render wiring. Clearing refills with the material's base color; the original texture pixels are not restored.
3130
+
3131
+ ### Audio — positional emitters, listener falloff, buses
3132
+ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})` — `{ sounds: Record<string, SoundDef>, buses?: Record<string, AudioBusDef> }` — declares the sound catalog (`SoundDef = { id, url, bus, gain?, loop?, positional?, falloff? }`) and mix buses (`music`/`sfx`/`ambient`/…, `AudioBusDef = { id, gain? }`) — both types from `@jgengine/core/audio/audioFalloff`. `entitySounds?: Record<string, string>` maps an entity **kind name** (same convention as `entitySprites`/`entityModels`) to a sound id: while a matching entity exists, the shell keeps a looping positional emitter on it, repositioned every frame. `objectSounds?: Record<string, string>` does the same keyed by placed-object catalog id. The pure distance→gain math (`computeFalloffGain(distance, config)`, curves `"linear" | "inverse" | "none"`) lives in core so it is unit-tested without a browser; `@jgengine/shell` (`shell/audio/audioEngine`, `shell/audio/AudioComponents`) is the only package that touches Web Audio — it owns an `AudioContext`, mounts `AudioListener` on the camera every frame, and `EntityAudioEmitters`/`ObjectAudioEmitters` drive per-instance emitter gain from the core falloff function. `GamePlayerShell` wires all of this automatically from `playable.audio`/`entitySounds`/`objectSounds` — a game never touches `AudioContext` directly.
3133
+ ### Camera rigs (`camera` field of `defineGame({...})`)
3134
+ The shell ships a **rig library**; a game picks and tunes one through `camera` config, never by writing camera positions from `onTick`. Select with `camera.rig` (or the `perspective: "third" | "first"` shorthand) — or by config block alone (#207.8): supplying `camera.<rig>` selects that rig with no redundant `rig` field, checked in the table's order; an explicit `rig` wins when several blocks are present:
3135
+ | `rig` | For | Key config (`camera.<rig>`) |
3136
+ |-------|-----|------------------------------|
3137
+ | `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
3138
+ | `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
3139
+ | `topDown` | ARPG iso / top-down (Diablo IV, Hades II) | `topDown: { height, pitch, yaw, followSmoothing, zoom }` — decoupled follow; `pitch` is camera elevation (PI/2 = straight down, near 0 = grazing and boom-distance blows up past `frustum.far`) |
3140
+ | `rts` | Free-pan / edge-scroll (The Sims, Manor Lords) | `rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start, pan }` — `pan: false` turns it into a static backdrop camera: no WASD/arrow pan, no edge-scroll, no Q/E rotate, no wheel zoom, still re-centers on `followEntityId` if one resolves |
3141
+ | `shoulder` | Over-the-shoulder (Helldivers 2, Remnant II) | `shoulder: { shoulderOffset, distance, ads, side }` — ADS + shoulder-swap (V) |
3142
+ | `lockOn` | Souls-like strafe (Elden Ring) | `lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }` — yaw binds to player→target; WASD becomes strafe |
3143
+ | `chase` | Vehicle chase (Forza, Rocket League) | `chase: { distance, springDamping, fov: { base, max, speedForMax }, shakePerSpeed, view: "chase"|"cockpit"|"hood"|"rear" }` |
3144
+ | `sideScroll` | Fixed lateral follow — 2.5D platformer/beat-'em-up | `sideScroll: { distance, height, lookHeight, axis: "x"\|"z", followSmoothing, fov }` — reads no player input, follows like the other follow rigs (defaults to the local player) |
3145
+ | `observer` | Detached spectator/photo/kill-cam (#120) | `observer: { bind: { kind: "entity", entityId } \| { kind: "point", position }, distance, height, orbitSpeed }` — reads no player input, auto-orbits the bound subject |
3146
+ | `inspection` | Model-viewer / data-viz orbit (#207.7) | `inspection: { anchor: "target"\|"cursor"\|"center", target, initialDistance, initialPosition, min/maxDistance, min/maxPolarAngle, pan, rotateSpeed, zoomSpeed, dampingFactor }` — left-drag orbit, middle/right-drag pan (pan defaults on for this rig only), scroll zoom toward the anchor (`cursor` = zoom-to-cursor); orbits a fixed `target`, never reads player/entity state |
3147
+ | `none` | No camera rig mounted | HUD-only presentations or a game that manages its own camera; see `presentation: "hud"` below |
3148
+ **Frustum:** `camera.frustum: { fov?, near?, far? }` overrides the canvas camera; `far` defaults to 300, so any world whose content spans more than a few hundred units must raise it or distant settlements/terrain silently clip out of view. **Every rig accepts `followEntityId: null`** so avatar-less games (city-builders, card games, auto-battlers) still mount a camera. Leave `followEntityId` unset and the shell defaults it to `ctx.player.possession.active(userId)` every frame, so a possession swap (party control-swap, BG3-style) or a form's mesh/camera-relevant change re-targets the camera automatically — set it explicitly only to override that default. **Shake / trauma (#28):** every rig reads a shake channel; feed it from anywhere with `import { cameraShake } from "@jgengine/shell/camera"` — `cameraShake(amplitude, decayPerSecond?)` (amplitude 0..1) — or from React via `useCameraShake()`. Tune with `camera.shake: { maxOffset, maxRoll, decayPerSecond, exponent, frequency }`. **Cinematic (#29):** set `camera.cinematic: { keyframes: [{ position, lookAt, fov?, duration?, ease? }], loop? }` to play a scripted path over the active rig, and `camera.transitionSeconds` cross-fades the camera when the rig changes so mode swaps don't hard-cut. The pure rig math (shake decay, spring-arm, speed→FOV, offset/strafe, keyframe lerp) is exported from `@jgengine/shell/camera` for testing.
3149
+
3150
+ ## `GameContext` — the ctx surface
3151
+
3152
+ `createGameContext` (in `@jgengine/core/runtime/gameContext`) wires every system:
3153
+
3154
+ ```
3155
+ ctx.scene.object place, remove, move, rotate, get, list, subscribe,
3156
+ at, inBox, raycast, raycastAll, catalog
3157
+ ctx.scene.entity spawn, despawn, setPose, update, get, list,
3158
+ stats.{get,set,delta}, setTarget, getTarget, cycleTarget,
3159
+ canReceive, preview, effect, paint,
3160
+ willHitProjectile, fireProjectile, settleProjectile,
3161
+ distance, inRadius, hasLineOfSight, queryArc, moveToward,
3162
+ spawnPoseOf, resetToSpawn, resetAllToSpawn,
3163
+ form.{register,get,active,abilities,shapeshift,revert}
3164
+ ctx.game commands, events, feed, loot, trade, quest, social, chat,
3165
+ unlocks, economy, leaderboard, roster, store, cards, turn
3166
+ ctx.game.social friends, party, presence, emotes.play, worldInvites
3167
+ ctx.game.store set, delete, get, has, subscribe, mapSnapshot, arraySnapshot — game-defined
3168
+ keyed reactive store slot (any value type); mutations bump ctx.version()
3169
+ ctx.game.cards pile(id, config?) — lazily creates (config required on first call) or returns
3170
+ the existing notify-wrapped CardPile for id
3171
+ ctx.game.turn loop(id, config?) — lazily creates (config required on first call) or returns
3172
+ the existing notify-wrapped TurnLoop for id
3173
+ ctx.player userId, isNew, inventory, stats (modifiers), loadout,
3174
+ applyLoadout, movement (pose/aim), motion (impulse/setVerticalVelocity/setY/takePending),
3175
+ possession, cosmetics
3176
+ ctx.player.motion impulse(vy), setVerticalVelocity(vy), setY(y), takePending() — game-code
3177
+ seam into the shell's vertical-motion integrator; drained once per frame
3178
+ before gravity, so a jump pad or grapple release calls this from
3179
+ onTick/commands instead of touching y directly
3180
+ ctx.item use, weapon
3181
+ ctx.input publish(held), isDown(action), held() — per-frame held-action snapshot, polled from onTick
3182
+ ctx.world ground (TerrainField), groundHeightAt(x, z) — the canonical
3183
+ sampler for the game's declared world; environment worlds
3184
+ resolve their terrain field, every other world kind is 0.
3185
+ Use it for every spawn/placement/waypoint y — never
3186
+ hand-roll a noise sampler or hardcode y = 0 on relief
3187
+ ctx.camera follow(entityId | null), followedEntityId(), setCinematic(config), cinematic(),
3188
+ subscribe — runtime camera-follow/cinematic override; the shell reads
3189
+ followedEntityId() each frame, falling back to the static
3190
+ playable.camera.followEntityId when it returns undefined
3191
+ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
3192
+ setSpeed, cycleSpeed; after, every, at (game-time timers)
3193
+ ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
3194
+ ```
3195
+
3196
+ `content.itemById(id)` supplies `{ use?, weapon?, trade? }`; `content.entityById(id)` supplies `{ stats?, receive?, onDeath?, movement?, role? }`; `content.objectById(id)` supplies `GameContextObjectEntry` `{ proximityPrompt?, breakable?, slotInventory? }`. Build all three from your catalogs in `content.ts`. A placed object resolves its catalog entry via `ctx.scene.object.catalog(instanceId)`; `ctx.scene.object.at(x, y, z, tolerance?)` finds placed objects near a point (grid interaction, click resolution beyond the pointer service). `ctx.scene.entity.update(id, patch)` writes a shallow patch onto a spawned entity's mutable fields (e.g. `movement.walkSpeed`) without a full respawn — `scene/movementSpeed`'s `applyStatDrivenSpeed(deps, id, { baseSpeed, multiplierStat?, flatBonusStat? })` is the catalog-driven helper that recomputes and writes `movement.walkSpeed` from a stat-modifier pair each time a buff changes.
3197
+
3198
+ ### Two tiers: `ctx` runtime vs pure factories
3199
+
3200
+ The `ctx` surface above is the **stateful runtime** — it's what game code uses. Every subsystem it wires is *also* exported as a **pure factory** that `createGameContext` composes internally: `createTradeSystem`, `createDeathSystem`, `createEffectSystem`, `createProjectileSystem`, `createSpatialApi`, `createEntityStatsApi`, `createEntityStore`, `createObjectStore`, `createStats`, `createLoadouts`, `createLootRegistry`, `createQuestJournal`, `createSocial`, `createSlots`, `createInteriors` (plus stateless helpers beside each — `canAffordCosts`/`resolveBuy` in `game/trade`, `getStatValue`/`applyPoolDelta` in `scene/entityStats`, and so on). **Build a game through `ctx`, not these** — reach for the factories only for unit tests of pure game math, headless servers, or a custom runtime. Import the domain deep path (`@jgengine/core/combat/death`, `@jgengine/core/game/trade`, `@jgengine/core/stats/statModifiers`, …) and read the `.d.ts`; each is a real export in the published package.
3201
+
3202
+ `createSpatialApi`'s optional `grid: { cellSize }` opts `inRadius`/`queryArc` into a lazily-built x/z broadphase index over `candidates()` instead of a linear scan — worth it once candidate counts run into the hundreds+. The index is reused across calls until `invalidate()` is called, so call it after any position change (move, spawn, despawn); a candidate outside the index at query time still resolves exactly (never silently skipped), only a *moved* one can be missed until invalidated.
3203
+
3204
+ ## `loop` — lifecycle
3205
+
3206
+ ```ts
3207
+ export function onInit(ctx: GameContext) {
3208
+ ctx.item.use.register(itemUseHandlers);
3209
+ ctx.player.loadout.register(loadouts);
3210
+ for (const table of lootTables) ctx.game.loot.register(table);
3211
+ ctx.game.quest.register(quests);
3212
+ ctx.game.quest.bind("entity.died");
3213
+ ctx.game.feed.bind("entity.died");
3214
+ ctx.game.events.on("entity.died", (evt) => onEntityDied(ctx, evt));
3215
+ setupWorld(ctx);
3216
+ }
3217
+
3218
+ export function onNewPlayer(ctx: GameContext) {
3219
+ ctx.scene.entity.spawn("player_default", { id: ctx.player.userId, position: spawnPoint });
3220
+ if (ctx.player.isNew) ctx.player.applyLoadout(ctx.player.userId, "starterKit");
3221
+ }
3222
+
3223
+ export function onTick(ctx: GameContext, dt: number) {
3224
+ // AI, regen, respawn timers — dt is GAME time (see ctx.time). Never death detection (see entity.died)
3225
+ }
3226
+ ```
3227
+
3228
+ `onInit` runs once per boot; register everything there. Loot tables register through `ctx.game.loot.register` — `lootTable()` is a pure validating factory, there is no global side-effect registry.
3229
+
3230
+ ## `ctx.time` — the simulation clock
3231
+
3232
+ `onTick`'s `dt` is **game time, not real time**: the shell scales each frame's real delta by `definition.time.scale` (real→game seconds at 1×) and the live speed multiplier, so writing decay/regen/AI as `rate * dt` makes it obey pause and fast-forward for free — never read wall-clock in a tick. Configure via `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused?, daysPerYear?, seasons? } })` (all optional; default is real-time 1:1 with speeds `[1,2,3,4]`, 365-day years).
3233
+
3234
+ - **Continuous** work scales through `dt`. **Scheduled** work uses game-time timers: `ctx.time.after(sec, cb)`, `ctx.time.every(sec, cb)`, `ctx.time.at(gameSec, cb)` — measured in game-seconds, so 4× fires them 4× sooner and pause freezes them. Each returns a cancel handle.
3235
+ - **Controls** (drive from a HUD or a command): `pause()`, `play()`, `toggle()`, `setSpeed(mult)` (0 pauses), `cycleSpeed()`. Read state with `ctx.time.snapshot()` / `ctx.time.calendar()` (`{ day, hour, minute, second, dayFraction, year, dayOfYear, yearFraction, season? }`), or in React with `useGameClock()` → snapshot + `controls`. Speeding to 4× or pausing affects **everything** on the tick — no per-system wiring.
3236
+ - **Calendar year/season** rides the same day counter, no separate clock: `year`/`dayOfYear` fall out of `day` divided by `TimeConfig.daysPerYear` (default 365), `yearFraction` is progress through the current year (0..1). Setting `TimeConfig.seasons: string[]` (e.g. `["spring","summer","fall","winter"]`) slices the year into equal named segments and populates `calendar().season`; omit `seasons` and the field is absent — a living-world sim names its own season boundaries this way instead of a hand-rolled `dayOfYear % ...` module.
3237
+
3238
+ ### Beat clock — BPM signal + input quantization
3239
+
3240
+ `@jgengine/core/time/beatClock` is a separate, purpose-built signal from `simClock` — a BPM tick generator for rhythm games (Hi-Fi Rush–style quantized combat), not a day/pause clock. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` returns a `BeatClock`: call `advance(gameDt)` from `onTick` with the same **game-time** `dt` (never wall-clock) — it fires `onBeat(beatIndex)` once per newly crossed integer beat and returns a `BeatSnapshot` (`beat`, `beatIndex`, `bar`, `beatInBar`, `phase`). `createBeatInputBuffer<T>(beatDurationSec)` is the auto-correct input buffer: `buffer(action, nowSec)` quantizes an off-beat press to fire on the next beat tick (or immediately if pressed exactly on one); `advance(nowSec)` drains and returns every action whose beat has arrived. `nextBeatTime(nowSec, beatDurationSec)` is the underlying pure quantization function. Feed a music track's actual BPM in; the buffer is what makes an early/late input still land on-beat.
3241
+
3242
+ ## Content catalogs
3243
+
3244
+ ### Object catalog fields
3245
+
3246
+ | Field | Purpose |
3247
+ |-------|---------|
3248
+ | `id`, `model` | Canonical id, asset key |
3249
+ | `footprint` | `{ w, h, d }` placement bounds |
3250
+ | `snap` | `"grid"` \| `"free"` \| `"wall"` |
3251
+ | `solid` | Blocks movement |
3252
+ | `breakable` | `false` or `{ baseBreakTime, harvest, drops, dropsWhenUnmet }` |
3253
+ | `proximityPrompt` | Float UI + optional command invoke |
3254
+ | `slotInventory` | Attached container `{ slots, accepts }` created at place time (`object:<instanceId>`) |
3255
+
3256
+ Break resolution: `duration = baseBreakTime / (tool?.breakSpeed ?? 1)`; drops per `when` (`always` / `harvestMet` / `silkTouch` / `playerKill`); then `inventory.put` + `object.remove`.
3257
+
3258
+ ### Item catalog fields
3259
+
3260
+ | Field | Purpose |
3261
+ |-------|---------|
3262
+ | `id`, `kind`, `stack`, `model` | Basics; `stack` feeds `itemTraits.stackLimit` |
3263
+ | `use` | Game handler name dispatched by `item.use` (`"fireGun"`, `"castBolt"`, `"drinkPotion"`) |
3264
+ | `weapon` | Stats the handler reads via `item.weapon.getStat` — `damage`, `heal`, `reach`, `manaCost`, `projectile.{mass,gravity,fuseTime,settleOn}`, `explosion.{radius}` … |
3265
+ | `trade` | `{ buy?: {coins: 80}, sell?, shops?: ["shop_town"] }` |
3266
+ | `requires` | Unlock ids gating purchase/use |
3267
+ | `placesObject` | Object id placed from hotbar |
3268
+ | `rarity`, `baseType` | Read by the `worldItem` rarity render binding + loot filter when this item drops to the ground (#32/#33); `baseType` defaults to the item id when absent |
3269
+
3270
+ ### Entity catalog fields
3271
+
3272
+ | Field | Purpose |
3273
+ |-------|---------|
3274
+ | `movement` | `walkSpeed` (reaches spawn automatically), `poses?: ["standing","crouch","prone","running"]`, `aim?: ["hip","ads"]`, `frozen?: boolean` — a scene-instance-level movement lock (cutscenes, stuns, mount transitions); `movedWhileFrozen(entity)` (`scene/entityStore`) flags an entity whose velocity moved anyway despite `frozen: true`, catching a system that bypassed the lock |
3275
+ | `role` | `CatalogEntityRole` = `"player"` \| `"enemy"` \| `"hostile"` \| `"npc"` \| `"vehicle"` — catalog hostility class for targeting (`"enemy"`/`"hostile"` classify hostile in `cycleTarget`). Distinct from the scene *instance* `EntityRole` (`"player"` \| `"npc"` \| `"prop"`, in `scene/entityStore`) which drives input/camera binding — **possession** (`ctx.player.possession`) flips this instance role between `"player"`/`"npc"` on every control swap, so exactly one owned entity is ever the input/camera target |
3276
+ | `stats` | Stat declarations — bounded values: `{ health: { max: 120, min: 0 }, level: { max: 60, min: 1, current: 1 }, … }` — `current` optional, defaults to `max` |
3277
+ | `receive` | Per-effect absorption: `{ damage: { order: ["shield","health"], modifiers? }, heal: { order: ["health"] } }` — keyed by **game-defined effect ids**; presence = can receive |
3278
+ | `onDeath` | `{ drops: "table_id" }` or reason-aware `{ drops: [{ table, when: { reason: "player_kill" } }], command?: { name, when? } }` |
3279
+ | `wander`, `talkable` | AI descriptor; dialogue id sugar for a talk prompt |
3280
+
3281
+ ### Dialogue catalog
3282
+
3283
+ `entities/npcs/dialogues.ts` — `{ id, lines: [{ speaker, text } | { choices: [{ label, invoke: { command, args } | null }] }] }`. Choices invoke `quest.accept`, `trade.open`, etc. Types ship from `@jgengine/react/components` (`DialogueDef`, `DialogueChoice`, `DialogueLine`) so a game imports them rather than redeclaring — the `DialogueBox` component renders the same shape it types.
3284
+
3285
+ A choice may gate its branch behind a roll: `{ check: { modifier, dc, advantage? }, onSuccess?, onFailure? }` (`onSuccess`/`onFailure` default to `invoke` when omitted). `DialogueBox` rolls via `@jgengine/core/stats/rollCheck`'s `rollCheck({ modifier, dc, advantage }, rng?)` (d20 by default; `advantage`/`disadvantage` roll twice and take the high/low; a natural 1 or max-die result reports `critical`) when the player clicks a checked choice, then calls `onChoice(choice, result)`; game code resolves which command to run with `resolveDialogueInvoke(choice, result)` (also exported from `@jgengine/react/components`).
3286
+
3287
+ ## `scene.entity.stats` — bounded stats
3288
+
3289
+ ```ts
3290
+ stats.get(instanceId, statId) // → { current, max, min } | null
3291
+ stats.set(instanceId, statId, { current?, max?, min? })
3292
+ stats.delta(instanceId, statId, n) // → null | { reason } — clamps into [min, max]
3293
+ ```
3294
+
3295
+ Health, mana, xp, level, energy — any stat id declared on the catalog. Spawn seeds from the catalog (`current ?? max`). Combat writes through effects; non-combat (regen ticks, XP grants) calls `delta` directly.
3296
+
3297
+ **XP/level use the engine progression primitive.** `@jgengine/core/game/progression` ships `curve()`/`evalCurve()` (evaluate a game-owned XP-per-level curve *definition*) and `leveling()` (a level track over the bounded `xp`/`level` stats that reports overflow). You own the curve *numbers* in a catalog; the engine owns the overflow math — on level-up bump `level.current`, reset `xp.max` from the curve, push a `stat.levelUp` feed entry. Hand-rolling `xpForLevel`/`levelFromXp`/`xpToNextLevel` is the anti-pattern — those already exist. `LevelingConfig.thresholdMode` picks how the curve is read: `"perLevel"` (default) treats `xpForLevel(N)` as the incremental N-1→N cost, summed internally; `"cumulative"` treats `xpForLevel(N)` as the total lifetime XP to reach level N (0 at/below `startLevel`) and compares `xp.current` straight against those totals — pick this for a design that quotes "total XP to level N" tables.
3298
+
3299
+ `leveling({ …, thresholdMode: "cumulative" })` switches to lifetime-total semantics: `xp` is a running total instead of a per-level bank, and `resolve(level, xp)` walks upward from `level` to the highest level whose cumulative threshold `xp` clears — it never demotes, and clamps the returned `xp` to the max-level threshold once capped. Default is `"perLevel"` (unchanged, fully back-compat) — pick `"cumulative"` for a total-XP display (MMO-style "12,450 XP") instead of a resettable per-level bank.
3300
+
3301
+ `ctx.player.stats` is a different thing: **modifiers** (buffs, ADS zoom, walk-speed bonuses) via `base/add/remove/get` with expiries — never bounded current/max values.
3302
+
3303
+ ## Targeting (MMO tab-target)
3304
+
3305
+ Persistent per-entity session state — never a per-use input field.
3306
+
3307
+ ```ts
3308
+ ctx.scene.entity.setTarget(fromId, toId | null)
3309
+ ctx.scene.entity.getTarget(fromId) // → instanceId | null
3310
+ ctx.scene.entity.cycleTarget(fromId, { filter: "hostile" | "friendly" | "any", direction? })
3311
+ ```
3312
+
3313
+ Hostility comes from catalog `role` (`"enemy"`/`"hostile"` classify hostile). Input `tabTarget`/`clearTarget` actions route here. Handlers always read `getTarget(input.from)` — `ItemUseInput` deliberately has **no `to` field** (single source of truth, no client-supplied target to validate, three targeting models stay clean: aim for shooters, `queryArc` for melee, `getTarget` for MMO).
3314
+
3315
+ ## `item.use` — one verb for all usable items
3316
+
3317
+ ```ts
3318
+ ctx.item.use.register(handlers) // once in onInit; duplicate names throw
3319
+ ctx.item.use.can(ctx, input) // → { reason } | null
3320
+ ctx.item.use.use(ctx, input) // dispatches catalog `use` → your handler
3321
+
3322
+ type ItemUseInput = { from: string; itemId: string; inventoryId?: string; aim?: Aim };
3323
+ type ItemUseHandler<GameContext> = {
3324
+ can?(ctx, input): { reason: string } | null;
3325
+ apply(ctx, input): { state: GameContext; error?: string };
3326
+ };
3327
+ ```
3328
+
3329
+ **Handlers receive the full `GameContext` as state** and mutate through it. Handlers own ammo, cooldowns, range checks, and effect ids; the engine owns projectile geometry, stat clamp math, and `canReceive`.
3330
+
3331
+ | Handler | Engine calls |
3332
+ |---------|--------------|
3333
+ | gun | spend ammo → `fireProjectile` → `settleProjectile` |
3334
+ | grenade | `fireProjectile` (ballistic) → settle → `effect({ at, radius })` |
3335
+ | melee | `queryArc` + reach from `getStat` → `effect` per hit |
3336
+ | MMO cast | `getTarget(from)` → `stats.delta(mana)` → `effect({ to })` |
3337
+ | consumable | `effect({ to: from, effect: "heal", via: { amount: -n } })` |
3338
+
3339
+ Banned in the engine: `weapon.fire`, `consumable.use`, `game.combat.*`, per-weapon commands.
3340
+
3341
+ ### Skill-checks and QTE (timed/rolled minigames)
3342
+
3343
+ `@jgengine/core/interaction/skillCheck` models a moving-target-zone minigame (casting/reeling, active-reload): `evaluateSkillCheck({ trackWidth, zone, markerPeriod, window, zoneDriftPerSecond? }, elapsedSeconds)` bounces a marker back and forth over `markerPeriod` seconds and returns `{ success, timedOut, markerPosition, zone }` — `zone` itself can drift when `zoneDriftPerSecond` is set. It is pure: an `item.use` handler starts a session by recording `ctx.time.now()` (game-time, so pause/fast-forward apply for free) the first time it's pressed, and evaluates `evaluateSkillCheck` against the elapsed time on the next press to lock in success/fail — the session bookkeeping (a `Map<instanceId, startedAt>`) is game-owned, same pattern as an ability-cooldown map.
3344
+
3345
+ `@jgengine/core/interaction/qte` sequences discrete timed prompts: `evaluateQteSequence(steps: QteStep[], inputs: QteInputEvent[])` walks `{ id, action, windowStart, windowEnd }` steps against `{ action, at }` presses and returns `{ status: "success" }` or `{ status: "fail", atStep, reason }`; `pendingQteStep`/`qteProgress` read the currently-active step and fraction complete for UI.
3346
+
3347
+ `@jgengine/react` ships matching headless UI: `SkillCheckBar({ config, startedAt })` and `QteTrack({ steps, startedAt })` self-tick via `requestAnimationFrame` and read `ctx.time.now()` each frame — pass `className`/`trackClassName`/`zoneClassName`/`markerClassName` (or `stepClassName`/`activeClassName`/`doneClassName` for `QteTrack`) for the moving-zone/timing visuals the UI quality bar requires.
3348
+
3349
+ ### Capture and owned roster
3350
+
3351
+ `@jgengine/core/scene/captureCheck` — `captureChance({ hpFraction, catchPower, difficulty? })` returns a 0..1 probability (lower `hpFraction` and higher `catchPower` raise it, higher `difficulty` lowers it); `rollCapture(input, rng?)` rolls it. `@jgengine/core/scene/roster` — `createRoster()` is a persisted, per-owner store (`capture`, `release`, `list`, `get`, `setEquipped`, `equippedList`, `snapshot`/`hydrate`) wired onto the runtime as `ctx.game.roster`, distinct from `game.social.party` (session-ephemeral) — roster entries persist and are optionally equipped (deployed) independent of party membership.
3352
+
3353
+ A capture item's `item.use` handler composes the primitives instead of forking them: read the wild target's hp via `ctx.scene.entity.stats.get(target, "health")`, roll `rollCapture({ hpFraction, catchPower })`, and on success call `ctx.scene.entity.despawn(target)` + `ctx.game.roster.capture(ownerId, catalogId)` — the wild scene entity is removed and re-parented into the owner's persisted roster; the react `CaptureOdds({ chance })` component shows the live odds meter the UI quality bar requires.
3354
+
3355
+ ## Combat — effects, projectiles, death, feel, abilities
3356
+
3357
+ Combat primitives — effects & projectiles, death handling, melee/defense/telegraph feel, and abilities/resources/auto-target/resistance/run drafts. Full surface: **[reference/combat.md](reference/combat.md)**.
3358
+
3359
+ ## Loot
3360
+
3361
+ ```ts
3362
+ lootTable({ id, rolls?, entries: [{ item? | currency?, count: n | [min,max], weight }] })
3363
+ ctx.game.loot.register(table) // in onInit
3364
+ ctx.game.loot.has(id) / roll(id, rng?) / grantToPlayer(userId, drops, source?)
3365
+ ```
3366
+
3367
+ Tables colocate with their domain (`entities/enemies/loot-tables.ts`, `objects/loot-tables.ts`). Entities reference them via `onDeath.drops`; chests via a `loot.open` command arg. `grantToPlayer` fills declared inventories, grants currencies, and emits `loot.granted`.
3368
+
3369
+ ## Card, board & shaped-inventory primitives
3370
+ Pure, renderer-free structures for card, board, and deckbuilder games — they sit **beside** the slot inventory, not in place of it. All are immutable-reducer + thin-controller pairs, mirroring the two-tier ctx/factory model: use the `create*` controller in game code, reach for the exported pure functions (`draw`, `moveCards`, `tickTimeline`, `laneAggregate`, `runPipeline`, `placeShaped`) for unit tests and headless servers.
3371
+ ```ts
3372
+ // cards/cardPile — named ordered zones (deck/hand/discard/exhaust); seeded shuffle, hand limit, reshuffle-on-empty
3373
+ const pile = ctx.game.cards.pile("deck", { zones: ["deck","hand","discard","exhaust"], drawFrom:"deck", handZone:"hand", discardTo:"discard", handLimit:7, reshuffleFrom:"discard" });
3374
+ pile.reset(createCardPileState(pileConfig, { deck: ids })); // seed zone contents once, from onInit
3375
+ pile.shuffle("deck", seed); // seeded Fisher–Yates via pileRng — deterministic under the same seed
3376
+ pile.draw(5); // deck → hand, clamped to handLimit, reshuffles discard when deck runs dry
3377
+ pile.discard(ids); pile.exhaust(ids, "exhaust"); // Slay the Spire / Balatro lifecycle
3378
+ // cards/modifierPipeline — ordered { source, apply(value) → value } with an inspectable per-step trace
3379
+ const score = runPipeline({ chips: 10, mult: 1 }, jokers); // score.value + score.trace[i].{before,after,changed} for Balatro-style scoring readouts
3380
+ // board/laneBoard — N lanes, per-side power aggregate + optional per-lane LaneRule modifier (Marvel Snap / Inscryption)
3381
+ board.aggregate(lane, "player").total; board.outcome(lane).winner; board.lanesWon();
3382
+ // board/timelineBoard — N slots each on an independent cooldown, resolving in expiry order (The Bazaar auto-battlers)
3383
+ board.tick(dtMs); // → fires[] sorted by expiry time then slot index; multiple fires per slot per tick
3384
+ // inventory/shapedGrid — polyomino footprints, rotate, overlap-check, adjacency (Backpack Hero / Tetris inventory)
3385
+ placeShaped(grid, { id, value, footprint }, [col,row], rotation); // rotateFootprint / canPlace guard overlap + bounds
3386
+ gridAdjacencyQuery(grid).neighborsOf(id); // feeds synergy effects
3387
+ ```
3388
+ Reuse the engine's seeded RNG (`pileRng`) for anything random — never `Math.random()` in game logic. The React drag/rotate/drop/snap gesture layer over these lives in `@jgengine/react` (see UI section).
3389
+
3390
+ `ctx.game.cards.pile(id, config?)` is the runtime-wired accessor for `createCardPile`: lazily creates the pile on first call (`config` required then) or returns the existing one for `id` on every later call, and every mutation notifies `ctx.subscribe`/bumps `ctx.version()` — so a `useEngineState`-bound hand/discard view re-renders without a game-owned store. Reach for `createCardPile` directly only for a headless test or server; game code goes through `ctx.game.cards.pile`.
3391
+
3392
+ ## Puzzle primitives — cell grids and falling pieces
3393
+
3394
+ Two pure, renderer-free `@jgengine/core` primitives for cell-based puzzle games (Tetris wells, match-3 boards); tile art and the drop-cadence loop are the shell's/game's job.
3395
+
3396
+ - **`puzzle/cellGrid`** — a generic immutable `CellGrid<T>` for uniform typed-cell boards. Row 0 is the top; `y` grows downward. `createCellGrid`, `cellAt`, `withCell`/`withCells` (immutable single/batch writes), `fullRows`/`clearRows` (line-clear + compaction), `collapseColumns` (match-3 cascade gravity), `findRuns` (run detection with an optional custom matcher).
3397
+ - **`puzzle/fallingPiece`** — the falling-piece layer over a `CellGrid`: `ShapeTable<TShape>` maps rotation states to cell offsets; `pieceCells`/`pieceCollides`/`mergePiece` place, test, and commit a piece; `dropDistance` computes the ghost-piece landing row; `gravityInterval`/`levelForLines`/`lineScore` are the classic Tetris drop-speed/level/score curves (overridable); `createLockDelay`/`stepLockDelay` is the grounded→countdown→lock stepper (`delaySeconds: 0` locks instantly on touchdown).
3398
+ - **`tactics/fallingGrid`** — a generic tile-drop grid over any `TCell` payload (distinct from the `cellGrid`/`fallingPiece` row-clear pair): `createFallingGrid(config)`, `gravityIntervalMs(level, config?)` for the drop-speed curve, and a `FallingGridSnapshot`/`LockState` shape for the grounded→lock stepper.
3399
+
3400
+ ## Dropped items — `worldItem` and the loot filter
3401
+ A `worldItem` is a scene **entity** (position + item ref + rarity), never an inventory item or object — see the three buckets. `onDeath.dropMode: "world"` (above) is the usual producer; games can also hand-place ground loot (chests, quest drops).
3402
+ ctx.scene.worldItem.spawn({ itemId, position, rarity?, baseType?, count?, affixTier?, source? })
3403
+ ctx.scene.worldItem.get(instanceId) / list() / nearestInRadius(from, radius, filter?)
3404
+ ctx.scene.worldItem.pickup(instanceId, userId) // grants to inventory + despawns, emits worldItem.picked_up
3405
+ Click-to-grab is engine-owned: setting `pointer.grabWorldItems: true` in `defineGame({...})` makes `@jgengine/shell`'s `GamePlayerShell` resolve `pointer.worldHit()` on primary click, and — when the hit entity is a `worldItem` within the `worldItem.pickupRadius` (default `DEFAULT_PICKUP_RADIUS`) configured on `defineGame({...})` of the local player — calls `pickup` directly, no game command needed. `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt off the same store.
3406
+ Presentation is a two-layer render binding, both engine-owned (rendered by `@jgengine/shell`'s `WorldItems`) over **game-supplied data**:
3407
+ 1. **Rarity baseline** — the `worldItem.rarityStyle: Record<rarity, { color?, beam?, label? }>` field of `defineGame({...})`, the game's rarity palette (Borderlands/Diablo-style beam + color coding).
3408
+ 2. **Loot filter overlay** (#33) — the `worldItem.filter: LootFilterRule[]` field of `defineGame({...})`, built with `lootFilter([{ id, when: { rarity?, baseType?, minAffixTier?, maxAffixTier? }, hide?, color?, beam?, label? }])` from `game/lootFilter`. **First matching rule wins** (PoE/Last Epoch block semantics); a rule only overrides the fields it sets, everything else falls back to the rarity baseline. `resolveWorldItemPresentation(item, rarityStyle, rules)` composes both layers and is what the shell calls per item.
3409
+ ## Gear systems — durability, affixes, modular items, storage tiers
3410
+ Four pure primitives that hang off item **instances** (not the stackable catalog id) — all catalog-first (specs are game-supplied config) and renderer-free. Item instances that carry durability/affix/modular state key off a game-assigned instance id, the same way targeting keys off entity instance ids.
3411
+ **Durability** (`item/durability`) — per-instance wear + repair. `DurabilitySpec` (`{ max, wearPerUse?, wearPerHit?, disableAtZero?, repair? }`) is catalog data; `createDurability(spec)` seeds a `DurabilityState`, `wear(spec, state, "use" | "hit", times?)` decrements (floors at 0), `isDisabled(spec, state)` gates use at zero, `durabilityFraction` feeds a HUD bar. Repair is quote-then-apply: `repairQuote(spec, state, { station?, to? })` returns the `{ item, count }[]` material cost (scaled by points restored) + the post-repair state (optional `qualityLossPerRepair` shrinks `max` each repair, Tarkov-style) — the game charges the materials through inventory, then commits the quote's `state`. `createDurabilityTracker()` keeps `DurabilityState` per instance id for the runtime.
3412
+ **Affix roller** (`item/affix`) — procgen `base × rarity → { rolled affixes, computed stats, name }`. `createAffixRoller({ pools, rarities })` over rarity-weighted `AffixPool`s. `roll(base, rarityId, rng)` draws `affixCount` distinct affixes without replacement (weighted, via the engine's `pickWeighted`), computes stats (base × `rarity.statScale`, then `op: "add"` affixes, then `op: "mul"`), and composes a name from `rarity.namePart` + prefix/suffix parts. `rollRarity(rng)` picks a weighted tier; `rollRandom(base, rng)` chains both. Pass `seededRng(seed)` for deterministic drops; any `() => number` rng works (same contract as `loot.roll`). `seededRng` lives in `random/rng` (re-exported here) alongside `seededStreams(seed)`, which derives independent named streams from one seed — `streams("worldgen")` vs `streams("history")` — so simulation draws never perturb generation (intervening in a run cannot change the map).
3413
+ **Modular item** (`item/modularItem`) — a whole assembled from parts in typed mount slots (guns, mechs). `ModularItemDef` has `slots: MountSlotDef[]` (`{ id, accepts, required? }`); `install(def, installed, slotId, part)` validates the slot exists, accepts the part's `category`, and is empty; `computeEffectiveStats(def, installed)` rolls part `stats` (additive) then `multipliers` over `baseStats`; `missingRequiredSlots`/`isComplete` gate a buildable whole. `createModularItem(def)` is the stateful wrapper (`install`/`uninstall`/`effectiveStats`/`partInSlot`).
3414
+ **Storage tiers + insurance** (`inventory/storageTier`) — the extraction-economy inventory half. Inventory containers carry a `tier: "carried" | "banked"` (`InventoryDeclaration.tier`; a Tarkov secure container is just a `banked` container on the body). `partitionOnDeath(containers)` splits a death snapshot into `{ kept, lost }` (banked survives, carried is dropped, stacks merged). `createDeliveryQueue()` is the delayed-delivery (insurance) hook: `schedule` a `ScheduledDelivery` with a game-time `deliverAt`, then `due(now)` / `claimDue(now)` drain it on the tick clock. `insureLost(lost, policy, userId, now, rng?)` filters the lost set to insured items and stamps a delayed `deliverAt` → feed straight into the queue. `resolveConsolation(policy, partition)` returns a baseline loadout id (apply via `applyLoadout`) — the death consolation grant, optionally gated on `if-carried-empty`. *(Session/round machines — extraction hold-to-leave, raid banking — consume this tier; see the objective-machine group.)*
3415
+ ## Objective, round & session machines
3416
+ Content-agnostic state machines for competitive/session shapes — plant/defuse, buy/live/end rounds, downed/revive, the battle-royale ring, extraction raids, run-vs-meta persistence. All pure `core`; every timer takes a **game-time** `dt`/`now` (`ctx.time`), so pause and fast-forward apply for free. Drive them from `loop.onTick` and pipe their events into `ctx.game.feed`/`events`; render their snapshots as HUD (per the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the downed banner, ring warning, and extraction timer are required HUD).
3417
+ **Contested channel** (`session/contestedChannel`) — the interrupt-on-damage progress objective behind plant/defuse, cash-out, urn deposit, banishing, and hold-to-extract. `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)` begins the channel, `tick(dt, occupants)` advances it against per-team occupancy (`Record<teamId, count>`) and emits `start`/`tick`/`contested`/`paused`/`complete` events, `damage(reason?)` interrupts (keeps or zeroes progress per `resetOnInterrupt`). `favorability[team]` scales fill rate (Deadlock deposit); `ratePerOccupant` fills faster with more owners present; `contested: "pause" | "decay"` chooses whether an opposing occupant freezes or reverses progress (The Finals contest). The owner leaving pauses it. Extraction hold-to-leave reuses this primitive verbatim.
3418
+ **Round state** (`session/roundState`) — the buy→live→end match machine (Valorant/CS). `createRoundState({ phases, teams, phaseOrder?, winCondition?, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs the phase timer and auto-advances (emitting `phase.start`/`phase.end`, rolling the last phase back into the next round's first), `concludeRound(winner)` records the win on any "conclude-eligible" phase (any phase but the first/last in the cycle), settles `round.economy` (winner gets `winReward`, losers get an escalating `lossBonus` via `lossBonusFor(rule, streak)` clamped to `max`), and moves to the next phase. `onPhaseEnd(hook)` fires commerce/spawn gates on each transition; `match.end` fires at `maxRounds`. `server.mode` stays a game string — this is the timer/economy engine under it.
3419
+
3420
+ Two extras beyond the default buy/live/end cycle: `phaseOrder?: string[]` overrides the phase names/cycle entirely (a wider `Record<string, number>` `phases` shape to match) — a draft→ban→play→score cycle is the same machine with different phase names. `teams: (string | { id, role? })[]` accepts a plain id or a `{ id, role }` pair; `roleOf(team)` reads the tag back (`"attacker"`/`"defender"`, Valorant side assignment) without a parallel lookup table. `winCondition?: (snapshot: RoundSnapshot) => string | null` lets `evaluate()` (call it from `onTick` alongside `tick(dt)`) auto-conclude the round the instant a score/objective condition is met, instead of the game hand-calling `concludeRound` — return a team id to end it, `null` to keep playing; `RoundSnapshot` is `{ round, phase, timeLeft, scores, lossStreaks, roles, matchOver }`.
3421
+ **Role assignment** (`session/roles`) — `assignRoles(players, specs: RoleSpec[])` distributes fixed-count or proportional roles (hider/seeker, spy/operative, prop/hunter) across a player list — the allocation half of an asymmetric session mode; `RoundConfig.teams`' per-team `role` is the lighter-weight alternative when a round machine already tracks the roster.
3422
+ **Downed / revive** (`combat/downed`) — the 3-state alive→downed→dead chain (Apex/Helldivers). `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down(id)` starts the bleedout, `tick(dt)` counts it down (→ `died`, optionally spawning a `banner`), `revive(id, dt)` accumulates an ally's hold time (→ `revived` with the health fraction the game restores), `finish(id)` executes a downed enemy, and `respawnFromBanner(id)` brings a banner-holder back at a beacon. It sits **in front of** the engine death resolution: on lethal damage call `down` instead of dying; on `died`/`bleedout` run the real `resolveDeath`. No banner ⇒ death is terminal.
3423
+ **Shrinking ring** (`session/ring`) — the battle-royale safe zone with out-of-bounds DoT. A catalog `RingConfig` is `{ center, phases: RingPhase[] }` where each phase is `{ startTime, shrinkDuration, fromRadius, toRadius, damagePerSecond, center? }` on the game clock. `ringSampleAt(config, t)` / `createRing(config).at(t)` returns the live `{ center, radius, damagePerSecond, shrinking }` (radius/center interpolate during each shrink window, hold between phases); `isOutside(t, pos)` / `distanceOutside(t, pos)` test a point, and `damageOutside(t, dt, positions)` returns per-entity `{ id, damage }` for everyone beyond the wall — feed those into `scene.entity.stats.delta`/`effect` each tick.
3424
+ **Extraction session** (`session/extraction`) — the raid-scoped "reach an extract and leave to bank what you carried" wrapper (Tarkov/DMZ/Helldivers), composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract(userId, extractId, team?)` opens a hold-to-leave channel, `tickExtract`/`damage` drive it, and on completion `resolveExtraction(userId, containers)` banks everything carried. `resolveDeath(userId, containers, now, rng?)` runs `partitionOnDeath` (banked kept, carried lost), schedules insured items through the built-in delivery queue (`claimDeliveries(now)` drains it on the clock), and yields the consolation loadout id. `playerSnapshot(userId)` feeds the extraction-timer HUD.
3425
+ **Persistence scopes** (`runtime/persistenceScope`) — the run-vs-meta split with explicit reset boundaries (Icarus mission wipe, Once Human season reset). `partitionScopes(state, { run })` splits a flat record into `{ meta, run }` by key; `resetRun` clears the run half while meta (talents/blueprints/account currency) survives; `clearRunFields(playerRow, runFields)` and `applyRunReset(profile, runFields, now)` do the same over `RuntimePlayerRow`/`PlayerProfileRecord`. `planScenarioReset({ gameId, serverId?, wipeChunks?, wipeServerSession?, resetPlayers?, runFields? })` normalizes a scenario/season reset that `HostPersistence.resetScenario?(reset)` applies — `@jgengine/sql` implements it (deletes the server's chunks + session, run-resets each profile in one transaction), keeping account meta intact.
3426
+
3427
+ ## Trade
3428
+
3429
+ Catalog `trade` fields drive everything — no duplicate price lists.
3430
+
3431
+ ```ts
3432
+ ctx.game.trade.canBuy(itemId, shopId, count?) // → reason | null
3433
+ ctx.game.trade.canSell(itemId, count?)
3434
+ ctx.game.trade.buy(itemId, count, { shop, inventoryId }) // charge → put, rolls back on failure
3435
+ ctx.game.trade.sell(itemId, count, { shop, inventoryId })
3436
+ ctx.game.trade.tradableAt(shopId, allItemIds) // derive stock from catalogs
3437
+ ```
3438
+
3439
+ ## Economy and unlocks
3440
+
3441
+ ```ts
3442
+ ctx.game.economy.balance(userId, currencyId) / grant(...) / charge(...) // charge → { reason } | null
3443
+ ctx.game.unlocks.has(userId, id) / grant(userId, id) / list(userId) / tree(categoryId)
3444
+ ```
3445
+
3446
+ Catalog `requires: [unlockId]` gates validate at command time.
3447
+
3448
+ ## Crafting, tech tree & production
3449
+
3450
+ Four **pure** primitives (no ctx, no renderer) for survival-crafting, tech-tree, factory, and farming games. All are catalog-first: recipes, tech nodes, production rates, and crop stages are game **data** you feed the primitive — the engine owns the graph math, the timers ride `ctx.time` (game-seconds), never wall-clock.
3451
+
3452
+ **Recipe graph** — `@jgengine/core/crafting/recipe`. A `RecipeDef` is `{ id, inputs: RecipeItem[], outputs: RecipeItem[], seconds?, station?, stationRange?, requires? }` — inputs + optional required-workstation-in-range + time → outputs. `craft(state, layout, traits, recipe, context)` consumes inputs and produces outputs on an `InventoryState` **atomically** (rejects `missing-inputs` / `no-station` / `locked` / `no-output-space` without mutating on failure); `canCraft(...)` is the dry-run. `context = { origin?, stations?, unlocked? }`: `stationSatisfied` checks a matching placed workstation (`{ catalogId, position }`) within `stationRange` of `origin`, and `requires` gates on `unlocked(id)` (wire it to `ctx.game.unlocks.has` or the tech tree). `createRecipeGraph(defs)` indexes recipes by `producing(itemId)` / `using(itemId)` / `category`. Long crafts schedule completion with `ctx.time.after(craftSeconds(recipe), …)`.
3453
+
3454
+ **Tech tree** — `@jgengine/core/economy/techTree`. **Generalizes flat `unlocks`, does not duplicate it**: a `TechNodeDef extends UnlockDef` adds `requires` (prerequisite node/unlock ids), an optional `recipe` payload, and `grants` (extra flat unlock ids). A node id **is** an unlock id, so flat unlocks are just tech nodes with no `requires`. `createTechTree(defs)` wraps `createUnlocks` internally and gates grants on prerequisites: `unlock(userId, id)` refuses until every `requires` is met, `available(userId)` is the reachable frontier, `recipes(userId)` lists the recipe payloads a player has unlocked (feed them to the recipe graph). `tree(categoryId)` and per-user `has`/`list`/`snapshot`/`hydrate` mirror `unlocks`.
3455
+
3456
+ **Production building** — `@jgengine/core/crafting/production`. `productionBuilding({ id, inputs, outputs, rate, power?, bufferMultiplier? })` — a placed building that consumes buffered inputs and emits outputs on a timer. `rate` is production **cycles per game-second**; `tickProduction(def, state, { dt, powered? })` advances continuously through `dt` (so pause/fast-forward apply for free) and completes as many cycles as the buffer allows. `feedProduction` / `drainOutput` move items in and out of the internal buffers (a puller/conveyor). `advanceTransport(path, items, dt)` slides items along a belt and splits off `delivered`. `resolvePowerGrid(supply, consumers)` powers demands greedily until supply is exhausted — gate a building's tick on `powered`.
3457
+
3458
+ **Farming** — `@jgengine/core/crafting/crop`. `CropTileState` is a soil state machine (`untilled` → `tilled` → planted); `tillTile` / `plantCrop` / `waterTile` are pure tile transitions and `advanceCropDay(def, tile)` runs the **day tick** — a `CropDef { stages, regrowDays?, needsDailyWater?, harvest? }` advances a growth stage per watered day and sets `harvestable`; `harvestCrop` yields and either clears the tile or resets a regrow crop. `applyToolToTiles(tiles, center, pattern, apply)` applies a tool across a tile pattern under the cursor — `singleTile()`, `squarePattern(r)`, `diamondPattern(r)`, `rectPattern(w,d)` (watering-can / hoe AoE). `createCropField(catalog)` is the stateful wrapper over a tile grid (`till`/`plant`/`water`/`harvest`/`advanceDay`); drive `advanceDay()` off the calendar day rolling over — `createDayTicker(startDay)` reports how many days `ctx.time.calendar().day` has crossed.
3459
+
3460
+ ## `applyLoadout`
3461
+
3462
+ ```ts
3463
+ ctx.player.loadout.register(loadouts) // onInit
3464
+ ctx.player.applyLoadout(userId, loadoutId) // → null | { reason }
3465
+ ```
3466
+
3467
+ `LoadoutDef = { inventories?: { hotbar: [{ item, count, slot? }], … }, stats?, economy?, unlocks? }`. Application is **all-or-nothing**: every inventory put dry-runs first; any rejection applies nothing. Starter kits gate on `ctx.player.isNew`; class/respawn kits run from commands. Never scatter raw `put`/`grant` calls for a kit.
3468
+
3469
+ ## Quests
3470
+
3471
+ ```ts
3472
+ ctx.game.quest.register(catalog) // onInit
3473
+ canAccept / accept / abandon / canTurnIn / turnIn / grant / revoke
3474
+ progress(userId, questId, objectiveId, delta)
3475
+ list(userId) / has(questId)
3476
+ bind("entity.died") // kill objectives match objective.target === catalogId
3477
+ bind("inventory.added") // collect objectives match objective.item
3478
+ ```
3479
+
3480
+ Catalog: `{ id, title, giver?, turnIn?, requires?, objectives: [{ id, kind, target?/item?, count, partyShare? }], rewards? }`. `requires` is satisfied by a completed quest of that id or an unlock. `turnIn` applies declarative `QuestRewards` — `{ xp?: { amount }, economy?: Record<string, number>, items?: { item, count, inventory }[], unlocks?: string[], quests?: string[] }` — note `xp` takes an `{ amount }` wrapper (applied via `stats.delta` + your level-up loop) and each reward `item` names the `inventory` it fills; chained `quests` are auto-offered if acceptable. Events: `quest.accepted` / `quest.updated` / `quest.completed`. `partyShare: { radius, credit: "all" | "tagger" }` extends kill credit to nearby party members.
3481
+
3482
+ ## Social
3483
+
3484
+ ```ts
3485
+ ctx.game.social.friends.canRequest / request / accept / decline / remove / block / list / requestsFor // persisted
3486
+ ctx.game.social.party.register({ maxMembers }) // then canInvite / invite / accept / decline / kick / leave / promote / list / membersOf / invitesFor
3487
+ ctx.game.social.presence.get(userId) // { online, serverId?, zoneId?, instanceId? }
3488
+ ctx.game.social.emotes.play(fromUserId, emoteId, radius?) // → { from, emoteId, at, recipients } | { reason }
3489
+ ctx.game.social.worldInvites.invite(fromUserId, toUserId, { serverId, joinCode? }) // then canInvite / accept / decline / listFor
3490
+ ```
3491
+
3492
+ Party is ephemeral session state (invites expire; leader leaving promotes the next member). Events: `social.friend.added`, `social.party.joined`, `social.party.left`.
3493
+
3494
+ **World invites** bridge friends and `multiplayer/matchmaking`: an invite carries the `{ serverId, joinCode? }` of the session you're in (the same fields as a `SessionListing`); `accept(userId, inviteId)` → `{ target }` is the join target you hand to your backend's `joinServer`/`joinByCode` — the invite never joins anything itself. Invites are ephemeral like party invites (TTL via `SocialDeps.worldInviteTtlMs`, default 60s; blocked users can't invite either direction). Events: `social.world.invited`, `social.world.accepted`. React: `useWorldInvites()` lists pending invites for the local player.
3495
+
3496
+ `emotes.play` reuses `scene.entity.inRadius` to find nearby **player**-role entities (default radius 20) and emits `emote.played` — never build a parallel proximity broadcast. Emote ids are game-defined strings (no registration, same convention as effect ids). Bind it into the existing feed primitive for a HUD feed: `ctx.game.feed.bind("emote.played")` + `useFeed({ action: "emote.played" })` — no dedicated emote hook exists or is needed.
3497
+
3498
+ ## Chat
3499
+
3500
+ ```ts
3501
+ ctx.game.chat.send(fromUserId, channelId, body) // → { message, recipients } | { reason }
3502
+ ctx.game.chat.whisper(fromUserId, toUserId, body) // stable per-pair channel "whisper:<a>:<b>"
3503
+ ctx.game.chat.history(channelId, { limit?, viewerUserId? }) // viewer filter drops blocked senders
3504
+ ctx.game.chat.register({ id, kind, radius?, historyLimit?, rateLimit? }) // custom channels
3505
+ ctx.game.chat.channels() / snapshot() / hydrate(data)
3506
+ ```
3507
+
3508
+ Built-in channels: `global` (everyone), `party` (reuses `social.party.membersOf`; rejects "not in a party"), `proximity` (reuses the same spatial/entity seam as emotes, default radius 20, **player**-role entities only). `kind` picks the recipient resolution; custom channels pick one of the three kinds. Sends are trimmed, capped (500 chars), and rate-limited per user per channel (default 10/10s, sliding window — `createChatRateLimiter` is the reusable pure primitive). Mute rides social's blocked set: blocked pairs can't whisper, and blocked senders are dropped from party/proximity recipients and from `history` when `viewerUserId` is passed. Every send emits `chat.message` (recipients omitted = broadcast). History is a bounded ring per channel (default 100) with `snapshot`/`hydrate` like `Friends`.
3509
+
3510
+ **Remote chat seam** (`multiplayer/chatContract`): `ChatTransport` is the hook-shaped contract (`useMessages(channelId | "skip")` / `useActions()`, identity-stable like `PresenceTransport`); `ChatSync` is the callback shape for backends that can't host React hooks. Bindings: ws — `createWsBackend(...).chatSync` / `.chatSyncFor(serverId)` over `chatSend` frames + a `chat` update channel (host relays per-channel rings, validates length + rate limit); Convex — `@jgengine/convex/convexChatTransport` `createConvexChatTransport({ messages, sendMessage })` (one live query + one mutation); local/dev — `createLocalChatTransport()`. React lifts a `ChatSync` via `chatTransportFromSync`.
3511
+
3512
+ ## Cosmetic loadout
3513
+
3514
+ ```ts
3515
+ ctx.player.cosmetics.register(defs) // onInit — Record<loadoutId, { slots: Record<slot, cosmeticId> }>
3516
+ ctx.player.cosmetics.apply(userId, loadoutId) // merges the preset's slots
3517
+ ctx.player.cosmetics.equip(userId, slot, cosmeticId | null) // set/clear one slot directly
3518
+ ctx.player.cosmetics.get(userId) // Record<slot, cosmeticId>
3519
+ ```
3520
+
3521
+ A per-player appearance layer distinct from `applyLoadout` (which grants inventory/stats/economy/unlocks) — cosmetics never touch gameplay state, only equipped slot ids for your renderer to read. Emits `cosmetics.changed`.
3522
+
3523
+ ## Possession
3524
+
3525
+ ```ts
3526
+ ctx.player.possession.own(userId, entityId) / disown / owns / listOwned(userId)
3527
+ ctx.player.possession.active(userId) // → entityId, defaults to userId itself
3528
+ ctx.player.possession.possess(userId, entityId) // → null | { reason } — must be owned + spawned
3529
+ ```
3530
+
3531
+ A player can own N scene entities (party members, vehicles, a possessed creature) and control exactly one at a time — distinct from `game.social.party`, which is a social grouping, not a control model. `possess` flips the previous/next entity's scene `EntityRole` between `"player"`/`"npc"` and emits `possession.swapped`; `@jgengine/shell`'s `GamePlayerShell` reads `active(userId)` every frame to rebind WASD movement, tab-targeting, hotbar `from`, and the camera rig's `followEntityId` to whichever entity is currently controlled — a game never wires this rebind itself.
3532
+
3533
+ ## Form / shapeshift
3534
+
3535
+ ```ts
3536
+ ctx.scene.entity.form.register(defs) // onInit — FormDef[] = { id, movement?, abilities?, model? }
3537
+ ctx.scene.entity.form.shapeshift(instanceId, formId, durationSeconds?) // → null | { reason }
3538
+ ctx.scene.entity.form.active(instanceId) // → formId | null
3539
+ ctx.scene.entity.form.abilities(instanceId) // → readonly string[] | null
3540
+ ctx.scene.entity.form.revert(instanceId) // early revert
3541
+ ```
3542
+
3543
+ A `form` bundles movement params + an ability-id list + a mesh into one swappable unit (shapeshift/transformation — V Rising bear/wolf/bat, Wukong's boss transformation). `model` reuses the entity's catalog `name` (the same key `entityModels`/`entitySprites` resolve against), so the mesh swap rides the existing render lookup — no parallel mesh field. `durationSeconds` is **game time**: it schedules the automatic revert through `ctx.time.after`, so it obeys pause and fast-forward like everything else on the clock. Emits `form.changed`.
3544
+
3545
+ ## Events, feed, leaderboard
3546
+
3547
+ ```ts
3548
+ ctx.game.events.on(name, handler) // register in onInit; typed GameEventMap
3549
+ ctx.game.feed.bind(action) // pipe an engine event into a ring buffer (default 20)
3550
+ ctx.game.feed.push(action, entry) // manual channels (chat, crafting)
3551
+ ctx.game.feed.recent(action, { limit? })
3552
+ ctx.game.leaderboard.track({ stat, scope: "global" | "server" | "profile" }) // onInit
3553
+ ctx.game.leaderboard.increment(userId, stat, { scope, by? }) / getTop / getProfile
3554
+ ```
3555
+
3556
+ `ctx.game.commands.define(name, { validate?(ctx, input), apply(ctx, input) })` registers a verb (`has`/`names`/`run` round it out); `run(name, input)` returns `{ status: "applied", state } | { status: "rejected", reason } | { status: "unknown-command" }`. `apply` may either **return** the next state (the classic reducer shape) or mutate `ctx` in place and return **nothing** — `run` keeps the current `ctx` as `state` when `apply` returns `void`, so a handler that only calls other `ctx` methods (spawn, effect, loot.grantToPlayer, …) doesn't need a pointless `return ctx`. **Event handlers use `ctx` directly** the same way (side effects: leaderboard, economy, scheduling) and never reassign state. One feed primitive for kill feeds, loot logs, quest updates — no per-domain feed hooks.
3557
+
3558
+ ## `ctx.game.store` — reactive game state
3559
+
3560
+ ```ts
3561
+ ctx.game.store.set("health", 100) // any key, any value type
3562
+ ctx.game.store.get("health") // T | undefined
3563
+ ctx.game.store.has("health")
3564
+ ctx.game.store.delete("health")
3565
+ ctx.game.store.subscribe(listener) // change-signal fires on set/delete
3566
+ ctx.game.store.mapSnapshot() / arraySnapshot()
3567
+ ```
3568
+
3569
+ A reactive per-game keyed store (`ObservableKeyedStore<unknown>`) attached to `GameContext` — reach for it instead of a module-level singleton store for ad-hoc reactive game state (turn trackers, deck UIs, anything that doesn't already have a `ctx` surface). `set`/`delete` bump `ctx.version()` and notify `ctx.subscribe` listeners; `get`/`has` are plain reads. Unlike a per-slot handle, there is no `define`/seed step — a key simply doesn't exist until the first `set`.
3570
+
3571
+ ## `ctx.game.cards` / `ctx.game.turn` — lazily-created piles and turn loops
3572
+
3573
+ `ctx.game.cards.pile(id, config?)` and `ctx.game.turn.loop(id, config?)` lazily create (config required on first call) or return the existing notify-wrapped `CardPile`/`TurnLoop` for `id` — call with just the id after the first `onInit` seed to fetch the same instance; every mutating method is wrapped so it bumps `ctx.version()`/notifies `ctx.subscribe` automatically, same as every other `ctx` surface. This replaces manually constructing `createCardPile`/`createTurnLoop` and wiring notification yourself.
3574
+
3575
+ ## Movement, pose, input
3576
+
3577
+ ```ts
3578
+ ctx.player.movement.getPose(id) / setPose(id, "crouch") // validates catalog movement.poses
3579
+ ctx.player.movement.getAim(id) / setAim(id, "ads") // ADS = aim state + zoom modifier, not a pose
3580
+ ```
3581
+
3582
+ Poses (`standing/crouch/prone/running`) change the collision capsule (`POSE_HITBOX`); aim pairs with a `player.stats` zoom modifier on `"reticle"`. Game code reads action names only (`isDown("aim")`, `wasPressed("interact")`) — hold vs toggle is resolved by the binding config, never by raw key branches.
3583
+
3584
+ ### `ctx.input` — polling the raw controls
3585
+
3586
+ `ctx.input` (`@jgengine/core/runtime/inputSnapshot`) is a per-frame held-action snapshot for `onTick` to poll, distinct from the command-dispatch path (bound actions still run commands the normal way): `publish(held: readonly string[])` (the shell calls this once per frame before `onTick`), `isDown(action)`, `held()` for the full list. Publishing never bumps `ctx.version()` — it's a poll surface, not reactive state.
3587
+
3588
+ **`repeatMs`** — bind an action as `{ hold: [...], repeatMs: 150 }` (`input/actionBindings`) and the shell fires its command on the down edge, then again every `repeatMs` while held, resetting on release (hotbar-style repeat-fire without a per-game timer).
3589
+
3590
+ **Aim on generic commands** — every command resolved from a bound action now runs with `{ yaw, pitch, aim: { yaw, pitch } }` in its payload, so a handler can read the camera-relative aim without going through `pointer.worldHit()`.
3591
+
3592
+ ### Controller kinematics — movement config, physics tuning, respawn
3593
+
3594
+ `PlayableGame.movement` (`PlayerMovementConfig`) tunes the shell's built-in walk controller (never touch `PhysicsWorld` for ordinary player movement):
3595
+
3596
+ ```ts
3597
+ movement: {
3598
+ mode?: "free" | "axis" | "grid"; // "free" (default) camera-relative; "axis" locks travel to one world axis; "grid" snaps each committed position to cell centers
3599
+ axis?: "x" | "z"; // world axis for mode "axis". Default "x"
3600
+ cellSize?: number; // cell size for mode "grid". Default 1
3601
+ collideObjects?: boolean; // collide the walking player against placed scene objects (unit-box AABBs) even without collision.voxel
3602
+ beforeCommit?: (frame: MovementCommitFrame) => readonly [number, number, number] | undefined | void; // intercepts each frame's resolved position before the pose commits; return a replacement to constrain/redirect the step
3603
+ }
3604
+ ```
3605
+
3606
+ `nav/navConstrain`'s `constrainToNavGrid(grid, { y? })` is a standalone walkable-pass-through + wall-sliding helper over a `nav/navGrid`; its `(proposed, entity)` shape doesn't match `beforeCommit`'s `(frame) => [x,y,z]` signature directly, so wire it in with a small adapter closure rather than passing it straight through.
3607
+
3608
+ `defineGame({ physics: { gravity, jumpVelocity } })` drives the kinematics controller directly: `gravity` (signed, e.g. `-24`) and `jumpVelocity` override the built-in tuning; omit either to keep the defaults. This is the one global exception to "never player tuning in `defineGame`" — it configures the shared controller, not a catalog entry. It is still **distinct** from `physics/physicsWorld`'s standalone rigid-body sim (see below) — `defineGame.physics` never touches that sim.
3609
+
3610
+ **Vertical motion intents** — `ctx.player.motion` (`@jgengine/core/runtime/motionIntents`): `impulse(vy)` adds to the vertical velocity the shell's controller is about to integrate, `setVerticalVelocity(vy)` replaces it outright, `setY(y)` wins over physics for that frame. The shell calls `takePending()` once per frame, before integrating gravity, to drain what accumulated; this is not reactive state (jump pads, launch abilities, bounce pads).
3611
+
3612
+ **`collision: { voxel: true }` — object lattice as solids.** When set, the shell's local player uses a voxel body whose `isSolid(x,y,z)` is rebuilt from `ctx.scene.object.list()` as exact `` `${x},${y},${z}` `` keys (integer cell queries). Integer-placed objects are walkable/blocking; fractional-coord objects decorate without colliding. Removing an object opens a real trapdoor under gravity — do not fake the fall with `setPose`. `visual.scale` does not shrink the collider (always a unit cell). The voxel body is created once; prefer `motion.setY` / `impulse` for vertical relocation — full XY teleport of the local voxel body is not supported. Solid cache rebuilds when object **count** changes. Recipe: `jgengine-newgame` → voxel trapdoor board.
3613
+
3614
+ **Respawn** — `ctx.scene.entity.spawnPoseOf(id)` reads the spawn pose, `resetToSpawn(id)` teleports back to it with zero velocity, `resetAllToSpawn(filter?)` does it for every entity matching an optional filter and returns the count (round resets, out-of-bounds recovery).
3615
+
3616
+ ## Touch & mobile
3617
+
3618
+ Every game is touch-playable with zero per-game input code. On a coarse-pointer device the shell derives a `TouchScheme` from the game's `input` bindings (`deriveTouchScheme`, `@jgengine/core/input/touchScheme`): a virtual joystick binds whichever of `moveForward`/`moveBack`/`moveLeft`/`moveRight` (or `turnLeft`/`turnRight`) are bound, on-screen buttons cover the remaining actions, and drag-to-look mounts automatically for `first`-person camera rigs. Touch controls feed synthetic `touch:<action>` codes into the same `ActionStateTracker` the keyboard uses — game code reads `isDown`/`wasPressed` and never branches on input source.
3619
+
3620
+ Refine the derived scheme with the `touch` field of `defineGame({...})` (`TouchControlsConfig`, all optional):
3621
+
3622
+ ```ts
3623
+ touch: {
3624
+ gestures: {
3625
+ tap: "rotateCw",
3626
+ swipeUp: "hold",
3627
+ swipeDown: "hardDrop",
3628
+ drag: { left: "shiftLeft", right: "shiftRight" },
3629
+ },
3630
+ buttons: [
3631
+ { action: "rotateCcw", label: "CCW" },
3632
+ { action: "softDrop", label: "Soft" },
3633
+ ],
3634
+ },
3635
+ ```
3636
+
3637
+ - **`gestures`** — bind `tap` / `swipeUp` / `swipeDown` / `swipeLeft` / `swipeRight` / `drag` (`{ left?, right?, up?, down?, stepPx? }`, repeats its action every `stepPx` of travel) on the play surface. An action consumed by a gesture is removed from the derived button set.
3638
+ - **`buttons`** — curate the on-screen cluster (order preserved; bare string or `{ action, label?, icon? }`); omit to auto-derive one button per remaining bound action. Buttons render a glyph, not text: `iconForAction` (`@jgengine/react/gameIcons`) resolves the action name to a `GameIconName` (`jump`, `sprint`, `rotateCw`, `hardDrop`, `swap`, `hand`, `restart`, arrows, …), the `label` becomes the `aria-label`; set `icon: "<GameIconName>"` to pick one explicitly or `icon: false` to force the text label.
3639
+ - **`hidden`** — actions to drop from the derived buttons without gesture-binding them.
3640
+ - **`movement: false`** — suppress the virtual joystick even when movement actions are bound.
3641
+ - **`look` / `lookSensitivity`** — drag-to-look on the play surface; defaults to `true` for `first`-person camera rigs, `0.005` radians/px.
3642
+ - **`touch: false`** — opt out entirely when the game's own DOM UI is already touch-native.
3643
+
3644
+ `useDisplayProfile()` (`@jgengine/react/display`) reports `{ coarsePointer, compact, portrait }` — live media-query state, SSR-safe — for adaptive HUD layout; see the mobile/touch rules in [`reference/ui-react.md`](reference/ui-react.md).
3645
+
3646
+ ## Interaction — `proximityPrompt`
3647
+
3648
+ One primitive for all float UI: `{ radius, display, invoke }` where `display` is `{ kind: "keybind", actionId }` | `{ kind: "gauge", gaugeId }` | `{ kind: "label", text }` and `invoke` is `{ command, args? }` or null (display-only). `talkable: "dialogue_id"` on an entity expands to a talk prompt. Engine picks the nearest prompt in radius (priority tie-break). Never build per-game hint resolver chains.
3649
+
3650
+ ## Pointer-driven input and navigation
3651
+ The **pointer is a service, not per-game glue**. Opt in with `camera` plus a `pointer` config in `defineGame({...})`; the shell casts the cursor into the world and dispatches commands you define — verbs stay commands, catalogs stay data.
3652
+ - **`pointer.worldHit()` (shell service).** The shell raycasts the cursor to `{ point, normal, entity, object }` (a renderer-free `PointerHit` from `@jgengine/core/input/pointer`) — entity/object are the topmost instance ids under the cursor, else `null`, with a ground-plane fallback for open terrain. Consume it renderer-free: `aimToPoint(origin, point)` builds an `Aim` for `item.use`/projectiles (ground-target skillshots, twin-stick), `groundOf(hit)` drops to `[x, z]` for routing. `pointer.worldHitCenter()` is the same raycast pinned to the viewport center instead of the live cursor — the reticle-aim query a locked/hidden-cursor rig (first-person, gamepad) needs when there is no cursor position to read.
3653
+ - **The `pointer` field of `defineGame({...})`** (all optional): `moveCommand` (left-click ground → `run(cmd, { point, entity, object })`, click-to-move), `select` (left-drag marquee + single-click box-select of entities), `orderCommand` (right-click ground → `run(cmd, { selection, point })`, issue a command to the selection), `contextMenu` (right-click an entity/object → its catalog `verbs` menu), `secondaryCommand` (right-click ground/entity/object → `run(cmd, { point, entity, object, aim })` when neither `orderCommand` nor `contextMenu` claims the click — a generic right-click verb for games with no selection/RTS model), `aim` (route the primary ability's aim to the cursor), `grabWorldItems` (left-click a `worldItem` within pickup radius → engine-owned `ctx.scene.worldItem.pickup`, no game command). Enabling `select`/`moveCommand` frees the left button for verbs; orbit moves to middle-drag.
3654
+ - **`createDragCapture({ maxPull?, grabRadius? })`** (`@jgengine/core/input/pointer`) — a renderer-agnostic slingshot/drawback state machine: `begin(origin, at)` starts a pull (rejected outside `grabRadius`), `update(at)` tracks the cursor, `release()` returns the final `DragState { origin, current, pull, magnitude, fraction }` (pull clamped to `maxPull`), `cancel()` aborts. Angry Birds-style slingshots, bow drawback, throwable wind-up — pair with `aimToPoint` to fire.
3655
+ - **Selection math** (`scene/selection`) is pure and testable: `createSelectionSet()`, `screenRect`/`selectWithinRect`/`isMarquee` over projected screen points.
3656
+ - **Context menu** (`interaction/contextMenu`): a catalog entity/object carries `verbs: contextVerb(label, command, args?)[]`; the shell builds the menu with `buildContextMenu` and dispatches the chosen command via `contextVerbInput` (verb args + `target`/`point`, so one handler can walk-then-act).
3657
+ - **Navmesh + A\*** (`nav/navGrid`): `createNavGrid({ bounds, cellSize, diagonal? })` → mark obstacles with `blockAabb`/`setWalkable`, or `populateNavGridFromEnvironment(grid, world)` to block every generated building's footprint on an `environment()` world's `structures` in one call (returns the count blocked) instead of hand-walking the district. `findPath(grid, from, to, { clearance?, smooth?, stepCost? })` returns a string-pulled `[x, z]` polyline (blocked start/goal snap to the nearest walkable cell) feeding **both click-to-move and AI routing**; `stepCost?(from, to)` multiplies the base cost of a grid step — `slopeStepCost(terrainField, weight?)` is the ready-made factory that penalizes steep terrain (routes around cliffs instead of over them). Renderer-free — AI and gameplay consume it without the shell.
3658
+ - **`pathFollow`** (`nav/pathFollow`): the lighter authored-polyline mover for tower-defense creeps that needs no navmesh — `createPathFollow({ waypoints, speed, loop? })` + pure `advancePathFollow(config, state, dt)` (crosses multiple waypoints per tick, reports `done`/`heading`/`distanceTravelled`). Feed it a navmesh route with `pathFromNav(route, elevation, offset?)` and the same follower drives click-to-move — `elevation` is either a fixed `y` or a `{ sampleHeight(x, z) }` field (any `TerrainField` qualifies), so a route across relief rides the ground instead of a flat plane.
3659
+ - **`constrainToNavGrid(grid, { y? })`** (`nav/navConstrain`) is a standalone walkable-pass-through + wall-slide helper: it passes through walkable moves, slides along walls at the navmesh boundary instead of stopping dead, and optionally remaps `y` to the grid. Its `(proposed, entity)` shape doesn't match `PlayerMovementConfig.beforeCommit`'s `(frame) => [x,y,z]` signature, so wire it in with a small adapter closure (see "Controller kinematics" above) to wall a player/AI to the same navmesh `findPath` already routes against.
3660
+ ## AI — director, threat, jobs, crowds (`ai/*`)
3661
+ Renderer-free AI over the same navmesh (`findPath`/`pathFollow`) gameplay already uses. Everything ticks on **game-time `dt`** (the `ctx.time` simClock delta), so it obeys pause and fast-forward for free. Manifests, patrol routes, job definitions, threat weights, and POIs are **game data** — the primitives own the loop, the catalog owns the content.
3662
+ - **Spawn director** (`ai/spawnDirector`) — budgets and escalates spawns for wave shooters and difficulty directors (Brotato, Bloons TD 6, Risk of Rain 2, Helldivers 2, Deep Rock Galactic). `createSpawnDirectorState(config)` then pure `advanceSpawnDirector(config, state, dt, { alive, players? })` → `{ state, spawns: SpawnRequest[] }`. Each `WaveManifest` grants a `budget` spent on affordable weighted `SpawnEntry`s (`cost`/`weight`/`minWave`), capped by `maxAlive`; `duration` auto-advances waves (or call `advanceWave` on "wave cleared"). Budget also trickles via `budgetPerSecond`, ramps a difficulty curve with `escalationPerSecond` (grows with sim-time), scales with `playerBudgetPerSecond`, and surges on `raiseAlert(state, amount)` decaying over time (bug-breach/dropship escalation). Seeded (`seed`) so ticks are deterministic. `pickSpawnPoint(points, players, { roll, bias })` biases placement toward (or away from) players. `config.spawnPoints?: NavPoint[]` lets the director pick a point itself: each `SpawnRequest` then also carries `point` (the chosen `[x, z]`, biased by `config.spawnPointBias`) and `laneId` (the point's index into `spawnPoints`) — feed multiple named lanes/portals and read `laneId` back to route the spawned entity down its lane instead of correlating positions by hand.
3663
+ - **Threat table** (`ai/threat`) — MMO/extraction aggro (Escape from Tarkov, WoW-style tanking). `createThreatTable({ decayPerSecond?, max?, forgetBelow? })`: `add(source, amount)` accumulates, `decay(dt)` bleeds off per game-second and forgets emptied sources, `highest({ current?, stickiness? })` returns the top-threat source to feed `scene/targeting` — `stickiness` (e.g. 1.1) keeps the current target until another exceeds it by that factor, so aggro doesn't jitter. `ranked()` for a threat meter.
3664
+ - **Patrol** (`scene/behaviors`) — `patrol({ waypoints, speed, loop? })` is a `BehaviorDescriptor` (a route is data) that layers a fixed beat on top of `wander`; drive it with `createPathFollow`/`advancePathFollow` (lane creeps, scav patrols in Deadlock/Tarkov). Route waypoints between guard posts with `findPath`.
3665
+ - **Job board** (`ai/jobBoard`) — colony/companion task assignment (Palworld stations, Schedule I employees, Sons of the Forest directives). `createJobBoard()`: `post(job)` a `JobDef` (`station`, `work` seconds, `priority`, `arriveRadius`, `repeat`), `claim(worker)` auto-pulls the highest-priority queued job or `assign(worker, jobId)` for a player order (steals it from its holder), `release` requeues. Per tick `advance(worker, dt, { distanceToStation })` runs the state machine `travelling → working → done` (path to `station(worker)` via `findPath`, occupy, run the loop), returning a `JobReport` on completion; `repeat` jobs re-run as a production loop and report each cycle.
3666
+ - **Crowd flow** (`ai/crowd`) — many agents routing to their own points of interest with congestion (Two Point Museum corridors, Dave the Diver seating). `computeFlowField(grid, goals, { clearance?, congestion? })` runs Dijkstra from the goals over the walkable grid → `direction(point)`/`next(point)` steer any agent toward the nearest goal (no per-agent A*). `createCrowdField(grid)` tracks per-cell occupancy (`enter`/`leave`/`count`); pass `crowd.penalty(weight)` as the field's `congestion` to reroute flow around crowded cells each tick. `selectPoi(pois, from, { roll, occupancy?, distanceBias?, distance? })` weights a POI by appeal and proximity, skips ones at `capacity`, and accepts a `distance` override (e.g. `findPath` length) to choose over the navmesh, not line-of-sight.
3667
+ ## Map, fog of war & ping
3668
+ Minimap/world-map/fog/compass state is renderer-free core (`world/*`), the top-down terrain image bakes in the shell, and the minimap/compass/world-map are react components. Ping rides the existing party + feed — it is not a new channel.
3669
+ - **Markers** (`world/markers`): `createMarkerSet()` is a reactive keyed set of `MapMarker { id, kind, position, label?, owner?, expiresAt?, meta? }` — `add`/`remove`/`get`/`list`/`query({ kind, owner, near, radius })`/`prune(now)`/`subscribe`. `kind` is a game-owned catalog string; `DEFAULT_MARKER_KINDS` (objective/enemy/loot/location/danger/ping/player/ally) supplies colors + glyphs the react map reads (override with your own `MarkerKindStyle` palette). Objective/entity/loot markers all live here.
3670
+ - **Fog of war** (`world/fog`): `createFogField({ bounds, cellSize })` is reveal-on-event — `reveal(x, z, radius?)` (a dig/act), `revealAlong(from, to, radius?)` (a walked trail); once a cell is revealed it stays revealed. `isRevealed`/`fraction`/`cells()` (stable snapshot for rendering)/`reset`/`subscribe`.
3671
+ - **Minimap math** (`world/minimap`): pure projection + bearings — `projectToMinimap(worldPoint, { center, worldRadius, size, rotate? })` → pixel `{ x, y, inside, distance }` (north = −Z maps up), `clampToMinimapEdge` for off-map markers, `compassBearing(from, to)`/`headingToBearing(yaw)`/`bearingToCardinal`/`relativeBearing` for the compass strip.
3672
+ - **Ping** (`game/ping`): `classifyPing(hit, { roleOf, categoryOf }, options?)` turns a G1 `pointer.worldHit()` `PointerHit` into a category (hostile entity → `enemy`, tagged object → its catalog category, open ground/ally → `location`). `createPingSystem({ markers, feed, party?, ttlMs?, classify, classifyOptions? })` composes classify + broadcast: `ping(from, hit, category?)` classifies, drops a categorized marker, and pushes the `PingPayload` to the party feed under `PING_FEED_ACTION` (`"party.ping"`) — the shell's feed bridge fans it to the squad. `DEFAULT_PING_CATEGORIES` is the enemy/loot/location/danger wheel. Enable the verb with the `pointer.pingCommand` field of `defineGame({...})`: the shell binds the `ping` input action → `worldHit()` → runs your command with `{ point, entity, object, normal }`.
3673
+ - **Shell render** (`@jgengine/shell/map`): `bakeTerrainMap(field, bounds, { resolution? })` renders a `TerrainField`/`RegionField` to a top-down PNG data-URL for the map background; `MapMarkerBeacons({ markers })` renders world-space beacons (the visible side of a ping) — wire via the `WorldOverlay` field of `defineGame({...})`. See the `extraction-map` demo game.
3674
+ ## Sensors, vision & observer tools (`sensor/`)
3675
+ Pure `@jgengine/core/sensor/*` primitives for querying and surfacing world state the player can't normally see or reach through the standard occlusion/proximity rules — reveal vision, hidden-state sensors, photo-mode framing, and session replay. Shell renderers/HUD pieces live in `@jgengine/shell/vision` and `@jgengine/shell/replay`.
3676
+ | Primitive | Answers |
3677
+ |-----------|---------|
3678
+ | `createRevealQuery({ resolvePosition, resolveTags, candidates })` → `RevealQuery` | `inRadius(center, radius, tags)` — occlusion-ignoring tagged-entity radius query (Dark Sight / detective-vision reveal, #115). `inRadius` already never checks occlusion (only combat's AoE `effect()` layers a LoS filter on top of it) — this is that same query shaped for a vision readout: scoped to catalog-declared tags, sorted nearest-first |
3679
+ | `probeHiddenState(origin, sources, { range, variableId, falloff? })` / `probeHiddenStateAll(...)` → `SensorReading \| null` | A sensor verb: reads a hidden zone/entity state variable (EMF/thermometer/geiger, #116) in range, strongest reading first; `strength` falls off linearly with distance by default |
3680
+ | `projectToView(camera, point)` → `FrustumProjection` | Pure camera-frustum projection (no three.js) — `inView`, `screenX/screenY` (-1..1), `distance` |
3681
+ | `framingScore(projection, config?)` → `number` | 0..1 framing quality from screen-center placement + distance-to-ideal (photo-mode "is this subject framed", #117) |
3682
+ | `createFrustumSensor(config?)` → `FrustumSensor` | `tick(camera, targets, dt)` — per-target in-view + framing + `dwellSeconds` (resets the instant a target leaves frame); a view-frustum sensor on a held camera object (Content Warning-style monster-filming scoring) |
3683
+ | `createRecordingBuffer(options?)` → `RecordingBuffer<T>` | `append(t, data)` / `seek(t)` / `range(fromT, toT)` — a session-recording buffer for replay/photo mode/kill-cam (#120), keyed on game-time so pause/fast-forward scrub consistently |
3684
+ | `colorDistance(a, b)` / `concealmentScore(entityColors, backgroundColors)` / `createConcealmentSensor(config?)` → `ConcealmentSensor` | Camouflage/blend-in scoring — how well an entity's palette matches its surroundings (hide-and-seek, stealth camo checks) against a `threshold` |
3685
+ | `createFreezeMonitor(config?)` → `FreezeMonitor` | Detects a tracked subject moving past a tolerance speed during a "freeze" window (red-light-green-light, statue games) and reports `FreezeViolation`s |
3686
+ Shell wiring: `@jgengine/shell/vision/RevealVision` (`RevealHighlights` — depth-test-disabled 3D highlight meshes for tagged entities in radius, meant for `WorldOverlay`; `RevealScreenTint` — full-screen CSS tint for "vision mode is on", meant for `GameUI`), `@jgengine/shell/vision/HiddenStateProbeHud` (`SensorReadoutMeter` — needle-strength HUD readout), `@jgengine/shell/vision/FrustumSensorHud` (`FrustumSensorReadout` — drives the sensor off the live render camera via `useThree`/`useFrame`, portals its HUD through drei's `Html fullscreen`), `@jgengine/shell/replay/useSessionRecorder` (records an entity's pose into a `RecordingBuffer` every frame; drive an observer-cam ghost, scrubber, or kill-cam export from it). The detached spectator/photo cam itself is the `observer` camera rig (see Camera rigs above) — bind it to any entity or fixed point.
3687
+
3688
+ ## World features
3689
+
3690
+ Renderer-free world surface — query primitives, environment fields + weather + realm composition, survival meters/moodles, interactive building & terraform, the optional headless physics world, vehicles/mounts/racing, and spawn placement. Full surface: **[reference/world.md](reference/world.md)**.
3691
+
3692
+ ## Turn-based & tactics (renderer-free)
3693
+
3694
+ Pure-`core` primitives for turn-based, grid-tactics, and card games — every one is a stateful factory with matching pure math, and every stateful piece exposes `capture()`/`restore()` so it plugs straight into the snapshot store. Overlays and tile art are the shell's/game's job; these ship the logic.
3695
+
3696
+ - **`turn/turnLoop` — `createTurnLoop(config)`.** An initiative machine over an ordered participant list with optional `phases` and per-turn action-economy `pools`. `advanceTurn()` walks the order (round++ on wrap) and **resets the entering participant's pools**; `advancePhase()` steps phases then rolls into the next turn. Pools are catalog data (`{ id, max, start? }`) — a single Slay-the-Spire energy pool or BG3's Action/Bonus/Movement/Reaction set, spent independently via `spend/canSpend/gain/refill`. `setOrder`/`addParticipant`/`removeParticipant` re-roll initiative without losing the active pointer. `config.onTurnStart?(participantId)`/`onTurnEnd?(participantId)` fire on every `advanceTurn()` transition (start also fires once for the initial participant at construction) — hang status-effect ticks, "your turn" banners, or AI-turn kickoff here instead of diffing `state()` between ticks yourself. `ctx.game.turn.loop(id, config?)` is the runtime-wired accessor: lazily creates (config required the first call) or returns the existing notify-wrapped loop for `id`, so a HUD bound to `ctx.subscribe` re-renders on every turn/phase/pool change with no separate store to wire.
3697
+ - **`turn/commit` — `createCommitController({ mode })`**, also hosted at `turnLoop.commit`. Three commit modes: `immediate` (submit resolves now), `simultaneous` (sealed hidden submissions → `reveal()` once `allReady()`, deterministic order — Marvel Snap), and `rewind` (visible `pending()` → `rewind()` to discard or `commit()` to finalize).
3698
+ - **`turn/intent` — `createIntentBoard()`.** A minimal per-participant "what will you do" board, lighter than a full commit round: `declare(participantId, { kind, magnitude?, targetId?, note? })` records one intent per participant (overwriting any prior undeclared one), `peek(participantId)` reads without clearing, `all()` lists every declared `[participantId, intent]` pair (for an enemy-intent HUD row, Slay-the-Spire style), `consume(participantId)` reads and clears in one call, `clear(participantId?)` clears one or everyone. Reach for this when you need visible declared-but-not-yet-resolved intents (telegraphed enemy actions) without the full simultaneous-reveal machinery of `turn/commit`.
3699
+ - **`tactics/tacticalGrid` — `createTacticalGrid({ width, height, blocked?, diagonal?, world? })`.** Tile occupancy (one unit per tile), `reachable(from, budget)` flood-fill (respects walls + occupants), `path(from, to)` shortest route, and `push(id, dir, { distance, chain })` discrete knockback-to-tile — chained collisions transfer momentum through struck units (Into the Breach), or stop with a recorded `PushCollision` against `wall`/`edge`/another unit. `world: { origin: [x, z], tileSize }` (mirroring `navGrid`'s bounds+cellSize convention) turns on `worldToTile(x, z)` (world point → `Tile | null`, null outside the grid) and `tileToWorld(tile)` (a tile's world-space center) — the render/pointer-hit round trip between the tactics grid and the 3D scene; omit `world` for a grid used purely as abstract logic (both throw if called without it).
3700
+ - **`tactics/predictiveQuery` — `predictAreaEffect`/`predictArcEffect`/`predictTiles`.** A "would-this-effect-hit" query for pre-commit overlays and enemy-intent telegraphs. It reuses the **exact** AoE/LoS targeting behind `ctx.scene.entity.effect` (`combat/effects` `resolveAreaTargets`) so the predicted target set matches what the effect would actually drain — without committing any state change.
3701
+ - **`tactics/snapshot` — `createSnapshotStore()`.** Cheap, repeatable turn-undo: `register(id, slice)` any `capture()/restore()` slice (the grid, surfaces, and turn loop all qualify), then `capture()/restore()` a deep-cloned snapshot or use the `push()/pop()` undo stack. `deepClone` handles objects/arrays/Map/Set so a held snapshot is immune to later mutation.
3702
+ - **`tactics/surface` — `createSurfaceLayer({ kinds, reactions })`.** A stateful tile surface layer with its own `tick(dt)` (timed surfaces decay + expire) and a **combination matrix** — `reactions` is data (`{ when: [a, b], result }`), so grease+fire→fire and water+lightning→electrified are catalog entries, not hard-coded. Distinct from terrain/water; drive its tick from `onTick`'s game-time `dt`.
3703
+
3704
+ ## External data — `data/dataSource` and the dev proxy
3705
+
3706
+ Renderer-free async-state primitives (`@jgengine/core/data`) for a game that reads a live external source (a leaderboard API, a session browser, remote config) — distinct from `ctx.game.store`/multiplayer, which are for the game's own authoritative state.
3707
+
3708
+ - **`createDataSource(load, options?)`** (`data/dataSource`) → `DataSource<T>` wraps one `load(signal)` async call as `{ status: "idle"|"loading"|"ready"|"error", data, error }`. `getState()` reads the current snapshot, `subscribe(listener)` fires on every change, `refresh({ force? })` re-runs `load` (de-duplicates a call already in flight unless `force`; aborts the prior call first when forced), `startPolling(intervalMs?)`/`stopPolling()` run `refresh` on an interval (`intervalMs` falls back to the one passed at construction; throws if neither is given), `dispose()` tears down polling and in-flight requests for good. Pass `options.clock` (`{ setInterval, clearInterval }`) to swap the timer source in tests.
3709
+ - **`fetchJson<T>(url, options?)`** (`data/fetchJson`) — `fetch` + JSON-parse in one call; throws `HttpStatusError` (`status`, `statusText`, `url`) on a non-OK response and `JsonParseError` (`url`, `cause`) on unparsable JSON, so a `DataSource`'s `error` is always one of these two typed shapes, never a bare `Error`. `options.fetchImpl` swaps the fetch implementation for tests/SSR.
3710
+ - **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
3711
+ - **Dev proxy (`data/devProxy`)** — same-origin routing for external APIs during `bun dev` so browser CORS never blocks a game's `fetchJson` call against a third-party host. `parseDevProxyTable(raw)` parses a `VITE_JGENGINE_DEV_PROXY` env value (a JSON object of `{ routeName: "https://api.example.com" }`) into a `DevProxyTable`; `proxiedUrl(target, { dev?, table?, prefix? })` rewrites a `target` URL whose prefix matches a table entry into `/proxy/<routeName>/<rest>` (default prefix `/proxy`) when `dev` is true (defaults to `import.meta.env.DEV`) — else returns `target` unchanged, so the same call hits the real host in production. `apps/dev`'s `vite.config.ts` reads the same env var and wires a matching Vite server `proxy` entry per route (`changeOrigin: true`, strips the `/proxy/<routeName>` prefix) — set `VITE_JGENGINE_DEV_PROXY` once and both sides (the URL rewrite and the actual proxy route) agree.
3712
+
3713
+ ## Multiplayer and the backend seam
3714
+
3715
+ The transport/host/persistence seam — `createWsBackend`, protocol codec, browser-safe authoritative host + router, WebRTC P2P, the Node/Convex/SQL adapters, presence, and save cadence. Full surface: **[reference/multiplayer.md](reference/multiplayer.md)**.
3716
+
3717
+ ## UI — `@jgengine/react`
3718
+
3719
+ The React layer — `GameProvider`, the hooks table, headless className-passthrough primitives (incl. map components), the identity/chat/voice/social/drag-layer kits, the shadcn registry install path for visual HUD components (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), the screen-layout rule, and the **UI quality bar** (required, not optional polish). Full surface: **[reference/ui-react.md](reference/ui-react.md)**.
3720
+
3721
+ ## Devtools — F2 overlay and tunables
3722
+
3723
+ `@jgengine/shell`'s `GamePlayerShell` mounts an F2-toggled debug overlay (`shell/src/devtools/DevtoolsOverlay.tsx`) over every game automatically — `defineGame({ devtools: false })` is the only way to turn the toggle off (default `true`). Five tabs:
3724
+
3725
+ | Tab | Shows |
3726
+ |-----|-------|
3727
+ | Perf | fps, frame/sim ms, draw calls, triangles, entity/object counts, state notifies/s, registered probes |
3728
+ | Tune | every discovered tunable — checklist grouped by source file or table export name; check one to control it live |
3729
+ | Logs | captured `console.log`/`info`/`warn`/`error` |
3730
+ | Net | observed backend round-trip latency (fed by `instrumentLatency`) |
3731
+ | Keys | the game's `ActionCodesMap` bindings |
3732
+
3733
+ **Tunables are zero-annotation.** Write plain code under `Games/<id>/src/**` — no import, no wrapper — and it's discoverable:
3734
+
3735
+ ```ts
3736
+ // loop.ts — top-level export const, nothing else
3737
+ export const GRAVITY = -22;
3738
+ export const SKY_COLOR = "#87ceeb";
3739
+ export const GOD_MODE = false;
3740
+
3741
+ // game/content.ts — an exported flat table of numbers/booleans/colors
3742
+ export const TUNING = { reach: 6, spawnRate: 0.4, fogColor: "#334455" };
3743
+ ```
3744
+
3745
+ The dev runner's Vite plugin, `tunableDiscoveryPlugin` (`@jgengine/core/devtools/transformTunables`, wired in `apps/dev/vite.config.ts`), rewrites each top-level `export const <number|boolean|"#hex">` literal to `export let` and binds it into the devtools registry as the module loads (`transformTunableExports` is the pure string transform underneath; `tunableModuleTable(id)` derives the table id from the file path, skipping `main.tsx` and `*.test.*`). Table exports need no transform at all — after each game module loads, the dev app calls `devtools.discover.scanModule(moduleExports)`, which walks every export's own properties for a flat plain-object table of numbers/booleans/`"#rrggbb"` strings.
3746
+
3747
+ F2 → Tune tab lists every discovered entry as a checklist, grouped by source file (top-level constants) or by table export name (object tables). Checking an entry hands it a live slider/toggle/color picker; unchecking resets it to its initial value. Kind is inferred from the value: `number` → slider, `boolean` → toggle, a `"#rgb"`/`"#rrggbb"`/`"#rrggbbaa"` string → color.
3748
+
3749
+ **Liveness.** An edit applies live wherever the code reads the constant/table entry at use time. A value captured once at init — passed into a function call, baked into worldgen — only picks up the new value on reload. Overrides persist in `localStorage` per game (key `jg-devtools:<game name>`) and are re-applied *before* `loop.onInit` runs, so even an init-baked constant respects its override after a refresh — *if* the read happens at or after `onInit`. A read that happens earlier than that (see below) never sees the override, reload or not.
3750
+
3751
+ **Default assumption: almost every gameplay number, boolean, and color is a tunable, not a hardcoded fact.** Walk speed, jump height, gravity, damage, cooldowns, spawn rates, drop chances, radii, durations, thresholds, multipliers, colors — if it's a scalar a designer would plausibly want to nudge while playing, it belongs in a place discovery can see (a top-level `export const`, or a direct scalar field on a catalog def object like `PlayerDef`/`EnemyDef`) — never buried as a bare literal inside a deeper nested object with no named export, and never computed once and thrown away. Treat "should this be tunable" as opt-out, not opt-in.
3752
+
3753
+ **Catalog-derived content must read fields live, not bake them at import time.** A common trap: a `content.ts` (or any module implementing `GameContextContent`) that loops over a catalog array *once at module scope* and copies scalar fields into a separately cached `Map`:
3754
+
3755
+ ```ts
3756
+ // WRONG — copies walkSpeed by value at import time, before devtools can even scan exports
3757
+ const entityEntries = new Map<string, GameContextEntityEntry>();
3758
+ for (const p of players) {
3759
+ entityEntries.set(p.id, { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } });
3760
+ }
3761
+ export const content: GameContextContent = {
3762
+ entityById: (id) => entityEntries.get(id) ?? null,
3763
+ };
3764
+ ```
3765
+
3766
+ This runs during module import — earlier than `discoverGameTunables`/override-application in `apps/dev/src/main.tsx`, and earlier than any `loop.onInit`. The catalog object (`p`) still gets live-mutated by devtools, but nothing ever re-reads it, so the baked `walkSpeed` is permanently stale: no F2 edit and no persisted override ever reaches gameplay, reload or not.
3767
+
3768
+ ```ts
3769
+ // RIGHT — map ids to the catalog def itself; build the entry fresh on every lookup
3770
+ const playersById = new Map(players.map((p) => [p.id, p]));
3771
+ function entityById(id: string): GameContextEntityEntry | null {
3772
+ const p = playersById.get(id);
3773
+ return p === undefined ? null : { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } };
3774
+ }
3775
+ export const content: GameContextContent = { entityById };
3776
+ ```
3777
+
3778
+ Same shape, same call site — the only change is *when* `p.walkSpeed` is read: at lookup time (each spawn) instead of once at import. Objects (`stats`, `receive`) are already reference-safe to pass through either way; this only matters for scalars (numbers/booleans/strings) copied out of a catalog def.
3779
+
3780
+ **`tunable()` still exists — an optional low-level primitive, not the recommended path.** Reach for it only when you need explicit bounds, an `options` select, or a change subscription that discovery can't infer:
3781
+
3782
+ ```ts
3783
+ import { tunable } from "@jgengine/core/devtools/devtools";
3784
+
3785
+ const gravity = tunable("physics/gravity", -22, { min: -60, max: 0 });
3786
+ ```
3787
+
3788
+ Read `gravity.value` at use time (or `gravity.subscribe(listener)`) — never destructure once at module load. A `"group/label"` name (e.g. `"physics/gravity"`) groups the control under `group` in the Tune tab; `devtools.controls.register` is the same call underneath. Real example: `Games/voxel-mine/src/loop.ts` — `tunable("mining/reach", REACH, { min: 2, max: 16, step: 1 })`, read via a getter passed to `createEditorHandlers`.
3789
+
3790
+ **Agent loop.** The overlay's "Copy report" button copies a JSON `DevtoolsSnapshot`; from a browser session an agent can instead call `window.__JG_DEVTOOLS.snapshot()` directly (or `snapshotDevtools()` from game code) for the same shape — frame stats, render sample, latency stats, captured logs, probe values, every registered control's current + initial value, and a `discovered` array (`id`, `kind`, `value`, `enabled`) covering every auto-discovered tunable whether or not it's enabled — a single call to check "is this actually working" without a screenshot. `window.__JG_DEVTOOLS.discover` is exposed directly too (`list`/`enable`/`disable`/`bind`/`scanTable`/`scanModule`/`clear`) so an agent can flip a discovered tunable on and read/write it from the console without touching the UI.
3791
+
3792
+ `devtools.probes.register("name", () => value)` (`@jgengine/core/devtools/devtools`) surfaces a game-specific gauge (entity count, queue depth, whatever) in both the Perf tab and the snapshot; call the returned unregister function to remove it.
3793
+
3794
+ ## Assets — real art from day one
3795
+
3796
+ Squares as enemies, colored boxes as buildings, and a flat grid floor read as *broken*, not unfinished. The blueprint's **Asset plan** names the packs before the first edit; a pass does not end while any default-material primitive, unstyled ground plane, or debug grid is visible in the staged screenshot — and that includes 2D HUD art: a first-letter tile, emoji, or one generic shape reused per slot is a placeholder exactly like a graybox enemy. Primitive stand-ins are allowed only *mid-pass* as scaffolding.
3797
+
3798
+ **Sources** (CC0 — public domain, commercial use, no attribution — unless noted):
3799
+
3800
+ | Source | What you get |
3801
+ |--------|--------------|
3802
+ | [Kenney.nl](https://kenney.nl) | 40,000+ CC0 assets: characters, buildings, nature, vehicles, weapons, UI, audio — the broadest single library |
3803
+ | [Quaternius](https://quaternius.com) / [KayKit](https://kaylousberg.itch.io) | CC0 low-poly packs incl. **rigged + animated characters**: medieval, sci-fi, dungeons, animals, adventurers |
3804
+ | [Poly Haven](https://polyhaven.com) / [ambientCG](https://ambientcg.com) | CC0 PBR textures, HDRIs, materials — the floor comes from here, never a flat color |
3805
+ | [Poly Pizza](https://poly.pizza) | Search engine over thousands of CC0 low-poly models for one specific thing |
3806
+ | [Game-Icons.net](https://game-icons.net) (CC BY 3.0 — credit it) / Kenney UI packs (CC0) | 4,000+ item/ability **icon silhouettes** — the registry `game-icon` item covers common HUD glyphs first |
3807
+ | [itch.io CC0 3D tag](https://itch.io/game-assets/assets-cc0/tag-3d) / [OpenGameArt](https://opengameart.org) | Long tail — **check the license per asset**, CC0 filter first |
3808
+ | [Mixamo](https://www.mixamo.com) | Free humanoid animations (Adobe license — fine for shipped games, not CC0) |
3809
+ | Kenney audio / [freesound CC0 filter](https://freesound.org) | Hit sounds, UI clicks, ambience |
3810
+
3811
+ **Rules:**
3812
+
3813
+ 1. **One style family per game.** Kenney + Quaternius + KayKit low-poly mix fine; low-poly models on photoreal PBR ground reads broken. Name the family in the blueprint.
3814
+ 2. **License discipline.** CC0 needs nothing; anything else gets a line in `src/game/assets-credits.md` (source, author, license). Never ship an asset you can't name the license of.
3815
+ 3. **Wire through the engine seams.** GLB models live in the game's `src/game/assets.ts` render catalog keyed by catalog id; billboards via `entitySprites`, real meshes via `entityModels`/`objectModels` in `defineGame({...})`; ground/skies belong to the world layer. Catalog `model` fields reference asset keys — never file paths in game logic. Source models through **`@jgengine/assets`** (`buildCatalog({ basePath })` → resolve ids/aliases → urls); `pull` packs into your app's `public/models/` (extracts Kenney's shared `Textures/` alongside the GLBs so models render textured). Network-restricted: `pull` falls back through `--mirror <baseUrl>` / `JGENGINE_ASSETS_MIRROR`, and `--offline` fails fast — see the package README for the fallback order and add/import flow.
3816
+ 4. **Coverage follows the content budget.** Every entity family, placed object, and held item maps to a real asset *before* the catalog entry ships. If the pack lacks a model, restyle the noun to one it has — rename the fantasy, don't ship a cube.
3817
+ 5. **Scale/pivot sanity.** `@jgengine/assets` measures each model's footprint/center/`minY` at reindex and ships them on the catalog entry (`catalog.resolve(id).dims`); with `objectModels` anchor `"center"` (the default) the shell centers the footprint on the placement point and ground-snaps the lowest vertex — so `object.place(id, cellX, y, cellZ, { rotation })` renders centered + grounded with no pivot math and no `dimensions.ts`. Anchor `"origin"` opts back into the raw GLB origin. Check the first placement of each model against its catalog `footprint`; one wrong pivot repeated 100 times is a rebuild.
3818
+ 6. **Item and ability icons are assets too.** Every hotbar/inventory/ability slot renders a real, distinct icon — the registry `game-icon` catalog (`iconForItemId`/`iconForAction`) or a Game-Icons/Kenney silhouette — per the UI quality bar's real-icons rule.
3819
+
3820
+ ## Genre cheat sheet
3821
+
3822
+ - **Voxel/crafting**: objects for blocks/machines, `voxel()`, `object.break`/`object.placeFromInventory`.
3823
+ - **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
3824
+ - **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
3825
+ - **MMO/RPG**: bounded stats + `leveling()` over a game XP curve; `tabTarget` → `cycleTarget`; handlers read `getTarget`; quests bound to `entity.died`/`inventory.added`; social party + `partyShare`; `server: "persistent"`.
3826
+ - **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
3827
+
3828
+ ## Anti-patterns
3829
+
3830
+ | Wrong | Right |
3831
+ |-------|-------|
3832
+ | Player tuning in `defineGame` | Entity catalog `movement` + stats |
3833
+ | `behaviors: […]` on place/spawn | Catalog entry |
3834
+ | Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
3835
+ | `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
3836
+ | `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
3837
+ | Polling HP in `onTick` for kills | `entity.died` event |
3838
+ | `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
3839
+ | Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
3840
+ | Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
3841
+ | Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
3842
+ | Kit seeding via scattered `put`/`grant` | `applyLoadout` |
3843
+ | Per-user quest state hand-rolled | `game.quest.register` + binds |
3844
+ | `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
3845
+ | Raw keys in game logic | `defineGame` input actions |
3846
+ | Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
3847
+ | Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
3848
+ | One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
3849
+ | Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
3850
+ | Half a system: quest without tracker, cooldown without sweep, keybind never shown, stub "coming soon" modal | Finish the system end to end — or cut it whole (see `jgengine-newgame`) |
3851
+ | Game-side workaround for a missing engine primitive | File the gap at github.com/Noisemaker111/jgengine/issues (or PR the primitive) and cut or scope the dependent system honestly |
3852
+ | Game nouns in this skill | Engine primitives + placeholder ids only |
3853
+
3854
+ ## New-game definition of done
3855
+
3856
+ This is a gate, not a suggestion — every box, in one pass (workflow: **`jgengine-newgame`** skill). "Compiles and the hooks are wired" is not done; a declared system with no UI, no feedback, or no way to exercise it is not done — finish the system or cut it whole.
3857
+
3858
+ - [ ] `game.config.ts` (`defineGame` from `@jgengine/shell/defineGame`) + `index.tsx` (barrel) + `main.tsx` (standalone host) + `loop.ts` + `game/content.ts`
3859
+ - [ ] Catalogs: `game/entities/<role>/catalog.ts`, `game/items/<domain>/catalog.ts`, `game/objects/catalog.ts`; loot tables beside their domain
3860
+ - [ ] Entity `stats` + `receive` orders aligned on the same stat ids; `role` set (drives targeting + camera)
3861
+ - [ ] `game/items/use-handlers.ts` registered in `onInit`; handlers read `getTarget`/`aim`, never a target input
3862
+ - [ ] `game/loadouts.ts` + `applyLoadout` in `onNewPlayer` (gated on `isNew`)
3863
+ - [ ] `game/quests/catalog.ts` + binds; if using xp/level, a game-owned curve fed to `game/progression` (`curve`/`leveling`) — **with their HUD/tracker, or cut**
3864
+ - [ ] `onInit`: register handlers/loadouts/loot/quests, event listeners, feed binds, leaderboard tracks; `setupWorld`
3865
+ - [ ] Player spawns with `id === ctx.player.userId`
3866
+ - [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
3867
+ - [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
3868
+ - [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
3869
+ - [ ] For an `environment()` world: a `<game>.world.test.ts` asserts `summarizeEnvironment(world)` (`@jgengine/core/world/environmentSummary`) is non-empty with the expected counts — the browserless scene-correctness gate
3870
+ - [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the final human glance, not the verification loop
3871
+ - [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
3872
+ - [ ] Multiplayer via adapter config only; no direct backend calls
3873
+
3874
+ ## Quick reference
3875
+
3876
+ ```
3877
+ defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
3878
+ + presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
3879
+ defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
3880
+ PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
3881
+ GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
3882
+ scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
3883
+ scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
3884
+ projectiles (object-aware raycasts); spatial queries (opt-in grid broadphase)
3885
+ entity.stats get / set / delta — bounded stats (health, mana, xp, level) on instances
3886
+ progression game/progression — curve() / leveling() over bounded xp/level stats
3887
+ item.use catalog `use` → GameContext handler; no input.to
3888
+ effects drain-signed magnitudes; receive.<effect>.order; AoE = effect + at/radius/los
3889
+ projectiles willHit → fire → settle; ballistic via weapon.projectile
3890
+ death onDeath (reason-aware drops/command), entity.died, auto kill attribution + drop grant
3891
+ game.loot register / has / roll / grantToPlayer (lootTable() = pure factory)
3892
+ game.trade canBuy / canSell / buy / sell / tradableAt
3893
+ game.quest register, accept…turnIn, bind(entity.died | inventory.added), declarative rewards
3894
+ game.social friends (persisted, requests listable), party (ephemeral, invites listable), presence, emotes (nearby broadcast), worldInvites (accept → join target)
3895
+ game.chat send / whisper / history / register — global/party/proximity channels, rate-limited, mute via blocked set
3896
+ game.roster capture / release / list / setEquipped — persisted owned-creature roster
3897
+ game.store/cards/turn store: keyed reactive slot; cards.pile(id): lazy CardPile; turn.loop(id): lazy TurnLoop
3898
+ game.events/feed/leaderboard on / bind+push+recent / track+increment+getTop
3899
+ devtools F2 overlay (Perf/Tune/Logs/Net/Keys); zero-annotation — top-level export const number/boolean/color + exported flat tables auto-discover into Tune; tunable() is the optional low-level escape hatch; snapshotDevtools()/window.__JG_DEVTOOLS.snapshot() → DevtoolsSnapshot (+ discovered[]); probes.register(name, read) adds a Perf gauge
3900
+ applyLoadout all-or-nothing kit seeding per userId
3901
+ player.movement pose (hitboxes) + aim (zoom modifier)
3902
+ player.motion impulse / setVerticalVelocity / setY — vertical-motion seam into the shell's frame driver
3903
+ player.possession own/disown/owns/listOwned + active + possess — control-swap, rebinds shell camera
3904
+ player.cosmetics register + apply/equip + get — per-player appearance slots, no gameplay effect
3905
+ scene.entity.form register + shapeshift/revert + active/abilities — movement+ability+mesh bundle, game-time duration
3906
+ proximityPrompt { radius, display: {kind}, invoke } — one float-UI primitive
3907
+ skillCheck/qte evaluateSkillCheck (moving zone + window) / evaluateQteSequence (timed steps)
3908
+ captureCheck captureChance / rollCapture — hp% + catchPower → probability
3909
+ dialogue check DialogueChoice.check (roll vs DC + advantage/disadvantage) → onSuccess/onFailure
3910
+ world features biomes / voxel / plots / tilemap / flat descriptors
3911
+ physics/physicsWorld optional headless rigid-body sim (PhysicsWorld) — not the defineGame physics field (gravity/jumpVelocity, honored by the kinematics controller); bodies are box (halfExtents) or sphere (radius)
3912
+ physics/ballisticSweep createBallisticSweep(world) → arc-vs-body hit test; wire into ProjectileSystemDeps.sweepBallistic
3913
+ anim/easing lerp/clamp01/smoothstep/tween/timedProgress + easeIn/Out/InOut Quad/Cubic + easeOutBack/Elastic — pure 0..1 tweening math
3914
+ data/dataSource createDataSource/createJsonDataSource — idle/loading/ready/error async state + polling; data/fetchJson + data/devProxy for CORS-safe dev fetches
3915
+ audio/audioFalloff computeFalloffGain / resolveEmitterGain — pure distance→gain curve; shell plays it
3916
+ time/beatClock createBeatClock (BPM ticks) + createBeatInputBuffer (buffered action → next beat)
3917
+ ws/voiceChannel createVoiceChannelRouter — positional falloff + simultaneous non-positional channels
3918
+ multiplayer/identity AuthSession + sessionPlayer + resolveGuestSession — Clerk/better-auth via react structural adapters
3919
+ multiplayer/chatContract ChatTransport (hooks) / ChatSync (callbacks) — ws + convex bindings, local for dev
3920
+ multiplayer/voiceContract VoiceTransport (join/leave/publish/subscribers) + createPushToTalk — media plane host-supplied
3921
+ GameBackend { transport, feeds?, presence? } — Convex is one adapter (createConvexBackend)
3922
+ adapter kinds offline / ws / convex / socketIo / p2p / lan (+ fly({app}) ws sugar) — runtime/adapter
3923
+ ws/pipe TransportPipe/TransportPipeFactory — any bidirectional string channel (webSocketPipe default)
3924
+ ws/host, ws/hostRouter browser-safe createGameHost + createHostRouter/loopbackPipe (node re-exports both)
3925
+ ws/peer createPeerHost/createPeerGuest — WebRTC P2P, host tab authoritative, copy-paste signal codes
3926
+ ws/socketIoPipe, node/socketIoServer socketIoPipe/createSocketIoBackend + attachGameSocketIoServer
3927
+ @jgengine/react GameProvider + hooks + headless primitives (incl. identity/chat/voice/social kits); layout only in GameUI.tsx
3928
+ ```
3929
+
3930
+ Engine ships verbs and primitives. Your game ships nouns.
3931
+
3932
+ # jgengine-api — Combat
3933
+
3934
+ Reference module for the [`jgengine-api`](../SKILL.md) skill. Load this when you need the combat surface — effects, projectiles, death, feel, and abilities.
3935
+
3936
+ ## Effects and projectiles
3937
+
3938
+ Effect ids are **game-defined strings**. Magnitudes **drain** stats: positive subtracts down `receive.<effect>.order` (spilling to the next stat in the order), negative restores. Heals pass a negative amount (`via: { amount: -flashHeal }`, typically read from a `weapon.heal` stat).
3939
+
3940
+ ```ts
3941
+ ctx.scene.entity.canReceive(instanceId, effect, magnitude?) // null | reason — reads catalog receive
3942
+ ctx.scene.entity.preview({ from, to, effect, via }) // magnitude, no state change
3943
+ ctx.scene.entity.effect({ from, to, effect, via }) // single target
3944
+ ctx.scene.entity.effect({ from, effect, via, at, radius, falloff?, los? }) // AoE at a point
3945
+ ```
3946
+
3947
+ AoE: `inRadius(at, radius)` → LoS filter (default on) → `canReceive` per target → absorption; `falloff: "linear" | "none"`. `via` = `{ item }` (magnitude from weapon stats) or `{ amount }`. `canReceive`'s `pools-depleted` reason checks headroom in the effect's direction: a positive (draining) magnitude needs a stat above its min, a negative (restorative) magnitude needs a stat below its max — so a heal can still raise a stat sitting at its minimum. Omitting `magnitude` assumes the draining direction.
3948
+
3949
+ `canReceive`'s optional signed `magnitude` makes the check direction-aware: positive (or omitted) keeps the standard `"pools-depleted"` check; negative checks the opposite direction and returns `"pools-full"` only when every stat in the receive order is already at max — so a heal (`via: { amount: -n }`) correctly reaches a fully-depleted target and is rejected only when there's nothing left to restore.
3950
+
3951
+ **Projectiles** (aim-based — no target ids):
3952
+
3953
+ ```ts
3954
+ willHitProjectile({ from, via, aim, effect }) // prediction only, for crosshair UI
3955
+ fireProjectile({ from, via, aim, effect }) // → shotId (pending)
3956
+ settleProjectile(shotId) // authoritative → { at, hits } | rejection
3957
+ ```
3958
+
3959
+ `Aim = { origin, direction } | { yaw, pitch, spread? }`. Hitscan settles into per-hit effects; ballistic shots (`weapon.projectile` with `fuseTime`/`settleOn`) settle to a landing point — the handler then calls `effect({ at: settle.at, radius })`. Settling twice rejects. Prediction is never authority.
3960
+
3961
+ Raycasts are **object-aware**: the default raycast checks placed scene objects as well as entities, discriminated by `RaycastHit.kind` (`"entity" | "object"` — an `ObjectRaycastHit` also carries `catalogId`). A crate or wall between shooter and target blocks or absorbs the shot instead of every projectile passing through scenery; supply `ProjectileSystemDeps.objects` (`{ list(), halfExtents?(catalogId) }`, matching `ObjectStore.list()` structurally) to opt in, with a `[0.5, 0.5, 0.5]` half-extent default per object.
3962
+
3963
+ ## Death
3964
+
3965
+ Resolved **once** by the engine when the last stat in the receive order hits min. No HP polling in `onTick`, ever.
3966
+
3967
+ - `entity.died` is emitted (before despawn — handlers can still read the victim's stats), then reason-matching `onDeath` entries run.
3968
+ - `DeathReason = { kind: "player_kill", killerUserId, via? } | { kind: "environment", source } | { kind: "self", source }`. Kills by the local player attribute automatically.
3969
+ - `onDeath.drops` tables are rolled and **granted to the killer** on player kills (emits `loot.granted`) when `onDeath.dropMode` is `"grant"` (default); `onDeath.command` runs through `ctx.game.commands`.
3970
+ - `onDeath.dropMode: "world"` routes item drops through a scatter impulse into ground `worldItem`s instead of straight to inventory (currency drops still grant directly) — tune the impulse with `onDeath.scatter: { radius, minRadius?, height? }` (defaults from `game/worldItem`'s `DEFAULT_SCATTER`).
3971
+ - Respawning under the same instance id revives it (it can die again). Same-id respawn must not happen synchronously inside the `entity.died` handler — defer a tick.
3972
+ - `quest.bind("entity.died")` credits kill objectives from the same event; leaderboards and kill feeds hang off it too.
3973
+
3974
+ ## Combat feel (melee, defense, telegraphs)
3975
+
3976
+ Layered on top of effects/projectiles/death — none of it replaces them, it adds **feel**. All models are renderer-free pure `@jgengine/core` factories a game composes per entity (like the `ctx`-vs-factory split above); the shell renders the telegraphs, styled damage numbers, hitstop shake.
3977
+
3978
+ - **Animation state machine** (`combat/animationState`) is the root the rest hangs on. A `AnimationClip` is catalog data — `{ frames, fps, ranges }` where each `FrameRange` tags a window `windup | active | recovery | cancel` (cancel may overlap recovery). `createAnimationState({ clips })` gives a per-entity SM: `play(clipId)`, `tick(dt)` → `{ entered, exited, completed }`, and queries combat/defense subscribe to — `inPhase("active")`, `isActive()`, `canCancel()`, `activeWindowMs()`. Frame ranges are the "commit frame" contract for delayed/feinted attacks.
3979
+ - **Attack tags** (`combat/attackTags`) — `attackMeta(["unblockable" | "thrust" | "sweep" | "grab" | …], { effect, power })`. Defense logic reads them: `isBlockable`, `isParryable`, `isDodgeable`, `counters(meta, "mikiri")`. A grab beats all defenses; an unblockable is parry/dodge-only.
3980
+ - **Defensive window** (`combat/defensiveWindow`) — a parry/block/dodge with `{ startupMs, activeMs, recoveryMs, iframes }`. `resolveDefense({ config, elapsedMs, attack })` is the pure overlap of the defender's window against the moment the attacker's `active` frames land → `parry | block | iframe | hit`. `createDefensiveWindow(config)` tracks the open time (`open(now)` / `evaluate(now, attack)` / `isInvulnerable(now)`).
3981
+ - **Combo strings** (`combat/comboString`) — `ComboStep`s with `cancelInto` + `cancelPhases` + optional `stance`, over the anim SM. `advanceCombo(...)` (pure) accepts the next attack only inside the current step's cancel window and matching stance; `createComboRunner(combo, anim)` drives the SM.
3982
+ - **Meters share one accumulator** (`stats/accumulatorMeter`) — `createAccumulatorMeter({ max, mode: "hold" | "reset", decayPerSecond, decayDelayMs, tiers })`: fill via `add(n)` → `MeterAddResult { fired, overflow, tier, tierChanged }`, `tick(dt)` decays after an idle grace. `combat/breakMeters` builds two on it: `createStaggerMeter` (mode `hold` — fills from hits, `broke()` stays true until `recover()` after a riposte/deathblow) and `createBuildupMeter` (mode `reset` — `add(n)` returns a `BuildupProc { status, durationMs }` at threshold for bleed/frost/rot, then decays). The same base backs G6's ult/streak meters.
3983
+ - **Dash / dodge** (`movement/dash`) — `createDashState({ distance, durationMs, iframes, staminaCost, staminaMax, staminaRegenPerSecond, cooldownMs })`: `tryDash(dir, now)` → burst or `{ reason: "no-stamina" | "cooldown" | "dashing" }`, `isInvulnerable(now)` for the i-frame window, `offset(now)` for the burst displacement, `tick(dt, now)` regens stamina.
3984
+ - **Hit reaction** (`combat/hitReaction`) — `resolveHitReaction(config, { attackerPos, targetPos, power })` (pure) → `{ hitstopMs, impulse, shake }`. Wired on `ctx`: `ctx.scene.entity.hitReaction({ from, to, config, power? })` knocks the target back and emits `combat.hitReaction` (the shell reads `shake` for a trauma channel — feeds a G2 camera-shake rig when present, else the shell's own kick).
3985
+ - **Telegraph** (`combat/telegraph`) — a `TelegraphShape` (`circle | ring | cone | line`) + `windupMs`. `ctx.scene.entity.telegraph({ from, shape, at, dir?, windupMs, kind?, effect? })` emits `combat.telegraph` for the shell to draw a ground decal that fills over the windup, and — if `effect` is bound — applies that effect to everyone inside the shape (`pointInTelegraph`) at activation. Returns a cancel handle. **Turn-scoped variant**, for tactics/turn-based enemy intents instead of a real-time windup: set `TelegraphConfig.turns` (a count of turns instead of `windupMs`), then read `telegraphTurnProgress(config, startedTurn, currentTurn)` (0..1 fill fraction), `telegraphFiredAtTurn(config, startedTurn, currentTurn)` (has it resolved), and `telegraphTurnsRemaining(config, startedTurn, currentTurn)` — the same "enemy will hit this tile" decal, advanced by `turnLoop.round()`/`active()` instead of a clock.
3986
+ - **Damage-number typing** — `ctx.scene.entity.floatText({ …, crit?, element?, hitType?, scale? })` (and the `entity.floatText` event) carry hit metadata; the shell's `resolveFloatTextStyle` (`@jgengine/shell/world/floatTextStyle`) maps crit/element to color + scale + glow.
3987
+
3988
+ ## Abilities, resources, auto-target, resistance, run drafts
3989
+
3990
+ Genre systems layered over the same effects/projectiles/targeting/loot primitives — all renderer-free pure `@jgengine/core` factories a game holds per player and ticks on **game-time** `dt` (so pause/fast-forward carry through). The ability kit is deliberately **separate from inventory items**: an item is a stackable id, an ability slot is cooldown/charge/resource state the HUD's four slot-states bind to.
3991
+
3992
+ - **Ability kit** (`combat/abilityKit`) — `createAbilityKit([{ id, cooldownMs, chargesMax?, resourceCost?, castType?, flashMs? }])`. `state(id, resourceAvailable?)` → `AbilitySlotSnapshot { state: "ready" | "cooldown" | "no-resource" | "just-cast", charges, chargesMax, cooldownRemainingMs, cooldownFraction, justCast, ready }`; `cast(id, resourceAvailable?)` consumes a charge, starts the recharge, and flashes just-cast; `canCast` / `tick(dt)` / `reset` / `retuneSlot(id, { cooldownMs?, resourceCost? })` (rebalance a slot at runtime — patches the config in place without touching charges or an in-flight cooldown timer, returns `false` for an unknown slot). The kit is resource-**agnostic** — it reports `no-resource` by comparing `resourceCost` to a supplied `resourceAvailable` (a mana stat, or an ult meter), and never spends the resource itself; the game's handler spends it and calls `cast`. Charges recharge one at a time; the four states drive the hotbar slot art. Cooldowns tick on `dt`, so hang `kit.tick(dt)` in `onTick`.
3993
+ - **Event-fed meters** (`stats/eventMeter`, built on `stats/accumulatorMeter`) — `createEventMeter({ max, mode, gains, resets?, tiers?, decayPerSecond? })`. `feed(tag, scale?)` maps a tagged combat event to a gain (or a reset when `tag ∈ resets`) → `EventMeterFeedResult { fired, ready, tier, tierChanged, reset }`. Mode `"hold"` is the **ult/adrenaline** economy — charges off `{ damageDealt, damageTaken, kill }` events, `ready()` at full, `consume()` spends it (Overwatch/Marvel Rivals). Mode `"reset"` is the **streak/combo** meter — builds on `kill`, resets on a break tag like `damageTaken`, climbs ascending `tiers` (Returnal adrenaline, DMC style rank). One primitive, two catalog configs.
3994
+ - **Auto-target policy** (`scene/autoTarget`) — `selectAutoTarget(policy, fromId, deps)` / `createAutoTargeter(policy, deps)` evaluated each tick with zero input: `"nearest" | "farthest" | "random" | "strongest" | "weakest" | "first" | "last"`. `deps` supplies `candidates`/`distance` and optional `strength` (health/threat) + `progress` (path progress for first/last-on-path). Bullet-heaven auto-fire (Vampire Survivors nearest), Bloons tower priority (first/last on path). Feed the picked id to `abilityKit.cast` + `effect`.
3995
+ - **Resistance matrix** (`combat/resistance`) — a damage-category × target-property table distinct from `combat/attackTags` (those are defense tags). `resolveResistance(matrix, category, targetProperties)` → `{ verdict: "immune" | "resist" | "normal" | "vulnerable", multiplier, immune }`; `resistanceScale` returns just the multiplier (default `immune:0, resist:0.5, normal:1, vulnerable:2`, overridable). Immune on any property wins; resists stack multiplicatively. Sits over the `receive` gate — the handler multiplies its effect `amount` by the scale (Bloons lead/camo immunities, elemental RPG weaknesses).
3996
+ - **Run draft** (`game/runDraft`, built on `world/scatterItems` `pickWeighted` + `stats/statModifiers`) — `createRunModifierStack(offers)` accumulates stacking picks (`add`, `count`, `atMax`, `total(stat)` aggregating adds × exponentiated multiplies, `apply(stats)` onto a `createStats` source). `createRunDraft({ offers, rng? })` adds the pause-flow: `present(n)` draws N distinct weighted offers (excluding maxed-out ones), `choose(id)` applies to the stack. Pause/resume is game-side (`ctx.time.pause()` before `present`, `play()` after `choose`). Vampire Survivors level-up picks, Hades boons, Risk of Rain stacking items.
3997
+
3998
+ # jgengine-api — World features
3999
+
4000
+ Reference module for the [`jgengine-api`](../SKILL.md) skill. Load this when you need the renderer-free world surface.
4001
+
4002
+ ## World features
4003
+
4004
+ Descriptors from `@jgengine/core/world/features` — config data the runner/world layer interprets:
4005
+
4006
+ | Feature | Use |
4007
+ |---------|-----|
4008
+ | `biomes({ map, zones, bounds? })` | Region atmosphere/rules layering; zones reference biome ids |
4009
+ | `voxel({ seed, generate?, streaming? })` | Block worlds |
4010
+ | `plots(config)` | Shared city + instanced interiors |
4011
+ | `tilemap({ map })` | 2D/2.5D levels |
4012
+ | `flat()` | Plain arena |
4013
+ | `environment({ terrain, sky, weather, vegetation, water, structures, pads })` | Composable outdoor scene — terrain + sky/time-of-day + rain/snow + grass + ocean + buildings + ground pads. Each field takes the matching descriptor: `terrain()`, `sky()`, `rain()`/`snow()`, `grass()`, `ocean()`, `building()`, `pad()`. `building()` and `ocean()` take `position: [x, z]` to site a cluster/water body away from the origin (several settlements, an offset lake); building clusters ground-snap to the terrain field per building; each `pad()` (a flat platform/paved patch — `{ center, size: [w,d] | { radius }, height?, color?, rotationY? }`) implicitly flattens the terrain beneath it via a `TerrainFlattenMask`, so a building pad or spawn circle never fights the noise field underneath |
4014
+
4015
+ `biomes`/`voxel`/`plots`/`tilemap` share a `WorldGridConfig` (`cells?: WorldGridCell[]`, `cellSize?`, `baseHeight?`, `defaultColor?`) — a `WorldGridCell` is `{ x, z, height?, color? }`, one extruded box per cell. `resolveGridInstances(config)` (`@jgengine/core/world/gridInstances`) is the pure cell→instance math (position, scale, color per cell); the shell renders the result as a single `THREE.InstancedMesh` **automatically whenever `PlayableGame.environment` is unset and `game.world` is one of these four grid kinds** — no manual render wiring for a cell-based world, same auto-render convention as `environment()` worlds.
4016
+
4017
+ `terrain()`'s `material` (a named palette — `"grass" | "sand" | "snow" | "rock" | "ash"`, resolved via `resolveTerrainPalette`/`TERRAIN_MATERIAL_PALETTES` in `world/terrain`) sets the default low/high/waterline colors; `colors: { low?, high?, waterline? }` overrides any of them field-by-field, and `segments` tunes the render mesh's subdivision. `flatten: TerrainFlattenMask[]` (`{ center, radius, height?, falloff? }`) carves explicit flat circles into the noise field independent of pads — building foundations, spawn circles, roads — blending back to the noise height over `falloff` (default `radius * 0.5`). `sky({ preset?, timeOfDay?, horizonColor?, zenithColor?, sunIntensity?, ambientIntensity?, fog? })` — `preset: "day" | "dusk" | "night"` (default `"day"`) is the static look; `timeOfDay: true` instead drives sun position, sky colors, and fog from the world clock's `calendar().dayFraction` every frame (`@jgengine/shell`'s `TimeOfDayDaylight` mounts this automatically for an `environment()` world with `sky` set — no per-game render wiring). **Gotcha:** `sunIntensity` / `ambientIntensity` are honored only on the **day** keyframe (`daylightCycle` builds dusk/night/dawn from fixed constants). Raising intensities under `preset: "dusk"` or `"night"` does nothing — use `preset: "day"` plus warm `horizonColor`/`zenithColor` when the first screenshot must be bright and readable (see `jgengine-newgame`'s first-shot art recipe).
4018
+
4019
+ `parentSpace` positions are local to that space — convert at seams only.
4020
+
4021
+ ### Query primitives (renderer-free, for gameplay)
4022
+
4023
+ Pure `@jgengine/core` functions so gameplay reads the same world the shell renders — no three.js needed:
4024
+
4025
+ | Primitive | Answers |
4026
+ |-----------|---------|
4027
+ | `resolveTerrainField(terrain(...))` / `noiseField(cfg)` → `TerrainField` | `sampleHeight(x,z)`, `sampleNormal(x,z)`, `waterLevel` — ground-snap, collision, camera. `resolveGroundStep` slope-limits movement |
4028
+ | `snapToGround(field, position, offset?)` → `[x,y,z]` | Replaces a position's `y` with the field's sampled ground height (+ `offset`) at its `x`/`z` — the pure version of a spawn/placement ground-snap |
4029
+ | `snapEntityToGround(entities, id, field, offset?)` → `boolean` | Ground-snaps an already-spawned entity in place via `entities.setPose` — false when `id` is unknown; the imperative sibling of `snapToGround` for "drop this entity onto the terrain right now" (mount dismount, teleport, respawn) |
4030
+ | `windField(cfg)` → `WindField` | `at(t)`, `atPoint(x,z,t)`, `strengthAt` — one wind source for weather sway, grass, sailing, fire spread |
4031
+ | `waterSurface(cfg)` / `waterSurfaceFromDescriptor(ocean(...))` → `WaterSurface` | `height(x,z,t)`, `normal`, `displace` — buoyancy, floating, shoreline (CPU Gerstner matching the ocean shader) |
4032
+ | `scatter(cfg)` → `ScatterPoint[]` | Seeded, overlap-aware point distribution — vegetation, props, lots, spawn points (`minDistance`, `avoid` rects) |
4033
+ | `createRegionField({ regions })` → `RegionField` | `sampleRegion(x,z)` blends content-agnostic biomes by nearest selector — height + `tint`/`water`/`fog`/`speedMultiplier` + opaque `data`. Extends `TerrainField`, so it ground-snaps too |
4034
+ | `scatterItems(field, area, layersFor)` → `ScatterInstance[]` | Region-driven content scatter — density per region, grounded, above-water/slope-aware. `pickWeighted` for weighted rolls. (vs `scatter`'s pure geometric points) |
4035
+ | `buildingIndex(district)` → `BuildingIndex` | `at`/`within`/`nearest`/`isInside`/`blockers` over a generated district — placement avoidance, pathfinding |
4036
+
4037
+ **Voxel field (`world/voxelField`).** `createVoxelField<T extends string>({ chunkSize? })` (default 16) is a logical block lattice for voxel games and instanced renderers — distinct from the `voxel()` `WorldFeature` descriptor above (that's the runner-level world kind; this is the block data structure a voxel game's gameplay reads and writes). `set`/`remove`/`get`/`has`/`fill`/`clear`/`count`/`cells`/`bounds` are the block CRUD (`set` returns `false` only when the identical type is already there — a no-op write). `neighbors(x, y, z)` returns the 6-adjacent occupied cells; `exposedFaces(x, y, z)` returns the `VoxelFace`s (`"px"|"nx"|"py"|"ny"|"pz"|"nz"`) not touching another voxel — feed that straight into greedy-meshing/face-culling. `raycast(origin, direction, maxDistance)` runs a 3D DDA and returns `{ x, y, z, type, face, adjacent, distance }`, where `adjacent` is the empty cell just in front of the hit — the placement target for block-place tools. Renderers dirty-track via `chunkOf(x, y, z)` + `chunkVersion(chunk)` and `subscribe(listener)`, so an instanced mesh only rebuilds the chunks that changed. For a non-`environment()` voxel world, assert on `field.summary()` (`{ blocks, types, bounds }`) the same way an `environment()` world asserts on `summarizeEnvironment` (see `jgengine-verify`).
4038
+
4039
+ **Destructible terrain (`world/carve`).** Two runtime-editable primitives for dig/carve worlds. `VoxelVolume` is a dense grid of material ids (0 = empty) — `carve({ center, radius, toolStrength })` clears a sphere of solid cells the tool is strong enough to break and returns the count removed (feed a loot roll), `deposit({ center, radius, material })` fills one (Deep Rock tunnels, Astroneer terrain add); `solidAtWorld` reads it back for collision. `CarvableField` (via `carvableTerrain(base)`) wraps any `TerrainField` and writes craters/mounds into its height — `carve({ x, z, radius, depth })`/`deposit({ x, z, radius, height })` — so ground-snap, collision, and the shell mesh all read the deformed surface (Helldivers 2 explosion craters). Cell strengths come from a `VoxelMaterial` table (DATA). Renders through `@jgengine/shell/terrain/CarvedTerrain`.
4040
+
4041
+ Renderers for these descriptors live in `@jgengine/shell` (`shell/terrain`, `shell/water`, `shell/weather`, `shell/structures`).
4042
+
4043
+ ### Environment fields, weather hooks & realm composition
4044
+ Renderer-free survival/environment primitives that extend the world query layer — meters, spawn gating, and damage-in-sunlight read the same world the shell renders, all ticking on game-time `dt`.
4045
+ - **Environment field** (`world/envField`): `createEnvironmentField({ dayLength, baseTemperature, nightDrop, altitudeLapse, terrain, rain, occluders, heatSources, ambientFloor, temperatureAt })` → `EnvironmentField`. Sample **temperature**, **wetness**, **lightExposure** (direct sun/sky), and **ambientLight** (spawn gating) at any `(x, z, time)` — `sample(x, z, time, y?)` returns all four plus `sheltered`. Occluders (roofs/canopy) shade sun and shelter from rain; heat sources (campfires) warm nearby positions; `sunElevation(time)` drives the day cycle. Sun damages a vampire, cold forces campfires, low ambient light spawns mobs — the field answers "am I in sun vs. shade / cold vs. warm / dark vs. lit". Pure and instantaneous; stateful build-up belongs to a decay meter reading the field.
4046
+ - **Weather → gameplay** (`world/weather`): `resolveWeather(state, table)` turns a `WeatherState { kind, intensity }` into concrete `ResolvedWeather` (`grip`, `visibility`, `structureDamage`, `chill`, `ignition`, `spread`) via a game-owned `WeatherModifierTable` — multipliers interpolate from neutral by intensity, rate effects scale linearly. Read `grip`/`visibility` in movement and AI, `structureDamage` on a building tick.
4047
+ - **Fire spread** (`world/weather`): `createFireGrid({ cols, rows, cellSize, origin, fuelAt, spreadRate, burnRate, wind, windBias })` → `FireGrid` is a **coarse cellular** propagation (not a fluid solver): `ignite(x, z)` / `igniteCell(col, row)`, then `step(dt, { spread, wetnessAt })` transfers heat to neighbours biased by wind, consumes fuel (`unburnt → burning → burnt`), and honours firebreaks (zero-fuel cells) and rain/wetness suppression. `resolveWeather(...).spread` feeds the step; `@jgengine/shell/weather` `FireSpreadLayer` renders the burning/scorched cells.
4048
+ - **Realm composition** (`world/realm`): `composeRealm(base, cards)` assembles a played instance at runtime from a deck of modifier **cards** (Nightingale realm cards) — a `major` card is the biome base, `minor` cards layer environment param overrides, a `WeatherState`, and spawn-table edits (`set`/`add`/`scale`/`remove`). The result recomposes both the environment (into a sampleable field via `composed.environmentField(extra?)`) and the `spawnTable`, and depends on the weather hooks above to turn its `weather` into gameplay modifiers.
4049
+ ### Survival meters, moodles & multi-region health
4050
+ The `survival/` domain — decay-over-time meters and per-part health, both feeding one stacking **moodle** status display distinct from numeric bars.
4051
+ - **Decay meters** (`survival/decayMeter`): `createDecayMeterSet([{ id, max, min?, start?, rate, thresholds }])` → `DecayMeterSet`. Each named meter (hunger, thirst, oxygen, sanity, warmth, stamina) drains/recovers on `tick(dt)` at `rate`, refills from consumables/actions via `refill(id, amount)`, and raises threshold moodles (`below`/`above`). `setRateModifier(id, mult)` lets the environment drive them — read an env field, then speed warmth loss when cold or oxygen loss in a toxic biome.
4052
+ - **Moodles** (`survival/moodle`): the shared status stack, distinct from raw bars. `stackMoodles(...groups)` folds meter, ailment, and buff `Moodle[]` into one worst-first display (same-id stacks add, worst severity wins). `createMoodleStack()` holds timed buffs (`add({ id, label, duration })` — Valheim's concurrent food buffs) and expires them on `tick(dt)`.
4053
+ - **Multi-region health** (`survival/regionHealth`): `createMultiRegionHealth({ regions, ailments })` → `MultiRegionHealth` gives per-part pools (head/thorax/arms/legs, Tarkov/DayZ style) — `damage(regionId, amount)` scales by `vulnerability` and kills when a `vital` part empties; a stacking **ailment queue** (`applyAilment`, `tick(dt)` drains like bleed) carries per-injury treatment (`treat(itemId)` clears wounds via bandage/tourniquet/splint). `ailmentMoodles()` shares the moodle display with the meters (#78 + #90).
4054
+ ### Interactive building & terraform (renderer-free tools)
4055
+ Turn data-only placement into the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum. All pure `@jgengine/core/world`; the shell renders the ghost/tint/brush (`shell/structures/PlacementGhost`, `shell/terrain/EditableGround`, `shell/terrain/TerraformBrushCursor`) driven by `pointer.worldHit()`.
4056
+ | Primitive | Answers |
4057
+ |-----------|---------|
4058
+ | `createPlacementController({ footprint, rules, snapMode, grid })` | Owns the ghost: `hover(hit)` → `PlacementPreview` (`valid` tint wraps `validatePlacement`), `rotate()`, `setSnapMode`/`cycleSnapMode` (`"grid"`/`"free"`/`"surface"`), `commit()` → `PlacementCommit` (`rotationY` via `quarterTurnsToRotationY`). Feed it `pointer.worldHit()`. |
4059
+ | `snapToNearest(registry, placed, movingDef, cursor, { snapDistance })` | Typed connector sockets — snaps a piece's socket onto the nearest **compatible** placed socket (`socketsCompatible` = both sides `accept` the other type). `worldSockets`/`socketWorldPosition` expand a piece's sockets to world space. |
4060
+ | `solveSupport(pieces, links, { maxDistance })` → `SupportResult` | Walks the connector graph to any `grounded` piece: `supported` stays, `unsupported` collapses, `distance` (hops-to-ground) drives the white→red decay tint. `toDebrisBodies(pieces, unsupported)` → `AddBodyOptions[]` for the `PhysicsWorld` debris sink. |
4061
+ | `createWallDrawTool({ snap, closeTolerance })` | Drag wall points → auto-encloses when the path returns to the start (`isEnclosed`), `footprint()` derives the room `EnclosedFootprint`, `roof()` auto-fits a hip/gable/flat `RoofPlan`. `createSurfacePaint()` stores per-tile floor/wall surfaces. |
4062
+ | `createPlacedStructureStore()` | Save/load a built layout: `add`/`move`/`rotate`/`remove`/`select`, `snapshot()`↔`load()` round-trip (survives reload), `subscribe` for the renderer. |
4063
+ | `createEditableTerrain({ bounds, base, cellSize })` → `EditableTerrain` | A `TerrainField` you can **write back to**: `apply(edit: TerraformEdit)` raises/lowers/flattens/paints under a cursor and re-samples `sampleHeight`; `surfaceAt`, `snapshot`/`restore`, `reset`. `createTerraformBrush(field)` is the cursor tool (`raise`/`lower`/`flatten`/`paint`, radius/strength). This write-back grid is the shared terrain-edit pattern. |
4064
+ | `createPlotPermissions({ plotId, ownerId, guildId? })` + `createContributionPool(goal)` | Per-plot/guild edit authority (`canEdit`/`canView`, `grant`/`revoke` `BuildRole`, guild inheritance) for co-op building, plus a pooled-resource contribution model (`contribute` caps at the goal, reports overflow, `isComplete`, per-contributor totals). |
4065
+
4066
+ ### Physics world (optional, headless)
4067
+
4068
+ `physics/physicsWorld` `PhysicsWorld` is a standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) — **not** the `defineGame` `physics: { gravity, jumpVelocity }` field, which the built-in walk controller reads directly every frame (see "Controller kinematics" above; both values are real and honored, not dead config). Reach for `PhysicsWorld` when a game needs many colliding dynamic bodies (piles, debris, stress scenes): `new PhysicsWorld({ capacity, bounds, … })`, `addBody({ position, mass?, ...shape })`, then `step(dt)` per tick → `PhysicsStats`. Core owns the sim; `@jgengine/shell/world/InstancedBodies` renders its bodies. Most games never need it — the character controller covers ordinary movement. The broadphase grid (`nx*ny*nz` cells from `bounds`/`cellSize`) throws at construction if it would exceed a sane cell cap — shrink `bounds` or raise `cellSize` (same guard on `physics/spatialGrid`'s `SpatialGrid`).
4069
+
4070
+ **Body shape: box or sphere.** `AddBodyOptions` is a discriminated union — `{ shape?: "box", halfExtents }` (box is the default, `shape` omittable) or `{ shape: "sphere", radius }` (the radius fills all three half-extent columns, so broadphase/bounds see the sphere's enclosing AABB). `world.shape[i]` (`SHAPE_BOX` / `SHAPE_SPHERE`) reports a live body's shape for a consumer walking the raw SoA arrays. Sphere-sphere and sphere-box pairs resolve with a proper radial normal (not the axis-aligned box/box path) — balls, projectile bodies, and rolling debris collide correctly against both boxes and each other.
4071
+
4072
+ **Collision shapes are box/sphere/voxel only.** Every collider in the engine — `PhysicsWorld` bodies (box `halfExtents` or sphere `radius`), object/entity picking (`scene/objectQuery` raycasts against unit boxes), and `world/voxelField`/`world/carve` blocks — is an axis-aligned box, a sphere, or a voxel cell. Arbitrary authored level-mesh collision (a sculpted GLB as a collider) is not supported. The seams for custom collision are `movement.beforeCommit` (steer or replace the walk controller's resolved step) and object raycasts (query the scene yourself and react) — not a mesh collider.
4073
+
4074
+ **Ballistic collision sweep (`physics/ballisticSweep`).** `createBallisticSweep(world, { step?, radius? })` → `BallisticSweep`, a `(origin, velocity, gravity, maxTime) => BallisticSweepHit | null` function that marches the closed-form arc (constant gravity, straight lateral) through a `PhysicsWorld` and reports the first sample inside any live body's AABB (sleeping bodies included), refined by one bisection step; `null` means the whole arc is clear. `step` (default 1/60) is the march interval in seconds, `radius` (default 0) inflates every body's AABB before the point test — pass the projectile's own radius. Wire it into `combat/projectiles` via `ProjectileSystemDeps.sweepBallistic`: when set, a ballistic shot settles at the swept impact point instead of the closed-form landing; omitted or `null` falls back to that closed-form arc.
4075
+
4076
+ **Removing and moving bodies.** `removeBody(id)` tombstones a body — it drops out of integration/broadphase and its slot is queued for the next `addBody` — without moving or invalidating any other body's `id` (ids are raw SoA slots, stored as-is in joints and game state, so nothing ever gets swapped). It conservatively wakes any sleeping body whose AABB touched the removed one's (no persistent contact set to consult, so this errs toward waking too much, never too little). `setVelocity(id, x, y, z)` and `setPosition(id, x, y, z)` write a body's velocity/position directly (instead of poking the public `velX`/`posX` SoA arrays) and wake it if asleep; `teleport(id, x, y, z)` is `setPosition` plus a hard velocity reset (respawn/teleporter, vs. sliding). `isAlive(id)`/`highWater` (one past the highest slot ever handed out) let a consumer that iterates the raw SoA arrays skip tombstoned holes correctly instead of assuming `count` is a dense `0..count` range.
4077
+
4078
+ **Joints & constraints.** `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(opts)` connect two bodies, or a body to a fixed world point (omit `bodyB`). The sim is translational (no angular DOF), so `hinge`/`fixed` pin the shared anchor (the `axis` is retained metadata), `distance` holds a fixed separation, and `spring` drives toward `restLength` with `stiffness`/`damping` (suspension, follow-point carry). `removeJoint(id)`, `setJointAnchor(id, x, y, z)` (move anchor B — a world anchor's follow point, or body B's local offset), `setJointAnchorA(id, x, y, z)` (move anchor A's local offset — e.g. re-rotating a suspension mount each frame as the chassis turns), `setJointRest`, and `readJointSegments(out)` for `@jgengine/shell/world/InstancedJoints` (debug line render). This is the foundation under vehicles, ragdolls, grapples, and carry.
4079
+
4080
+ **Collision → gameplay events.** `world.onCollision(listener, minApproachSpeed?)` delivers every impacting contact — `CollisionEvent { a, b, nx, ny, nz, approachSpeed, impulse }` — to game code during `step` (the object is reused; read/copy it, never retain). This is the seam crash-damage and destruction read; pass `null` to detach.
4081
+
4082
+ **Actors on top of the sim:** `physics/ragdoll` (`createRagdoll(world, { bones, links, balance? })` — jointed bones, floppy or active-ragdoll via a balance motor), `physics/carryable` (`Carryable` — grab a body to a follow point, shared multi-owner carry, `carrySpeedMultiplier` encumbrance, drop/throw; the raycast pick is the caller's job, core owns the constraint), `physics/forceVolume` (`ForceVolume` — impulse/velocity/accelerate trigger region, `once` for boost pads; `PlatformCarry` — carry bodies standing on a moving platform by its per-`step` delta). Separately, `physics/spatialGrid` `SpatialGrid` is a broad-phase grid over the x/z plane, **distinct** from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers — `rebuild(count, xs, zs)` then `queryCircle` (swarm enemies hitting a player/AoE) or `forEachPair` (mutual separation).
4083
+
4084
+ **Traversal (`physics/traversal`).** `Grapple` fires a rope from a body to a fixed world point on the joint API — `fire(x,y,z)` attaches a `distance` (rigid) or `elastic` (spring) joint, `reel(dt)`/`payOut(dt)` shorten/lengthen the rope to pull the traveller in, `moveAnchor` re-points it (ziplines, grapple-to-moving-target). Grapple/zipline/swing (Sekiro, Deep Rock, Just Cause) are all the same primitive; the raycast that finds the anchor is the caller's. `Glide` is a reduced-gravity, forward-thrust wingsuit/glider over a body — call `apply(dt, steerX, steerZ)` each frame before `step` to feed back most of gravity (`gravityScale`), thrust along the steer vector, and clamp descent; stop calling it to fall normally, no attach/detach state.
4085
+ **Structural destruction (`physics/structure`).** `StructureGraph` models a building as nodes (pieces) + load-bearing edges with some nodes `anchor`ed (foundations). `damage(id, n)`/`damageEdge(a,b,n)`/`severEdge(a,b)` wear pieces and connections; when one breaks, the graph recomputes reachability to an anchor and returns a single `CollapseEvent { fell }` — every piece the loss disconnected. `toDebris(world, event)` sinks the fallen pieces into a `PhysicsWorld` as rigid bodies (The Finals, Rainbow Six). It is coarse by design: **replicate the collapse event (the `fell` id list), not each fragment's physics** — game clients re-derive the debris locally. Piece integrity and edge strength default from a `StructureMaterial` table (DATA).
4086
+ ### Vehicles, mounts, crash damage & racing
4087
+ Five primitives layer a driving/racing game over the physics sim and `world/water`. All are **data-first** (spec the chassis/wheels/grip curve, damage thresholds, and checkpoint layout as catalog data) and pure `@jgengine/core`; renderers live in the game/shell. Each `update(dt, …)` runs **before** the shared `world.step(dt)`.
4088
+ - **Analog input — `input/axisInput`.** `AxisInput { throttle, brake, steer, handbrake }` is a continuous channel, **distinct from the digital action bindings**. `new AxisChannel({ bindings, smoothing })` ramps held keys into pedal-like analog values (`sample(dt, isDown)`), or `setAnalog(axis, value)` drives it straight from a gamepad axis. `DRIVE_AXIS_BINDINGS` is a ready WASD/arrow map.
4089
+ - **`physics/vehicleBody`.** `createVehicleBody(world, config)` is an arcade car: a chassis box body with per-wheel suspension held by G3's `springJoint` against the sampled `groundHeight`, drive/brake along the heading, and a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering — and, under `handbrake`, drift. `update(dt, axisInput)` then `world.step`. Because the chassis is a real body it still collides, which feeds crash damage. Rocket League, Trackmania, Wreckfest.
4090
+ - **`physics/buoyancy`.** `createBuoyantBody(world, { body, water, … })` floats a body on a CPU `WaterSurface` (Archimedes per hull point + water drag) so it settles at the waterline and rides the Gerstner waves; pass an `AxisInput` to `update(dt, time, input?)` and it drives as a boat (thrust + yaw + keel). Sea of Thieves, BOTW rafts.
4091
+ - **`scene/mount`.** `createMountController()` transfers control to a driven entity: `register({ id, kit, seats })`, `mount(riderId, mountId, seatId?)`, `dismount`. Read `cameraTarget(riderId)` to point the follow camera and `driveTarget(riderId)` to route that rider's `AxisInput` at the mount — the control seat drives, passenger seats ride (multi-seat shared vehicles), and an un-mounted rider drives themselves. `driver(mountId)`/`occupants(mountId)`/`kitOf`. Palworld mounts, V Rising horse, a crewed ship.
4092
+ - **`scene/stationClaim`.** `createStationClaim(controller?)` layers **facet stations** on `scene/mount` for a vehicle several players crew at once: `register({ id, kit, stations })` where each `Station` tags a seat with a `facet` (`"steer"`/`"sails"`/`"cannon"`). `claim(playerId, vehicleId, facetOrStationId)`, `release`, `controllerOf(vehicleId, facet)` (who mans it), `facetOf(playerId)`, `openFacets`, `crew`. Only a `control` station operates the hull (`driver`/`driveTarget`); the rest ride but command their own facet. Sea of Thieves helm + sails + cannons.
4093
+ - **`physics/damageZones`.** `createDamageModel({ zones, disableAt })` maps accumulated contact impulse (from `onCollision`) to **coarse discrete stages** (not soft-body): `absorb(zoneId, impulse)` / `routeCollision(event, resolveZone)` bump a zone's stage (caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once, and crossing `disableAt` flips a whole-vehicle `disabled` state. Wreckfest crumple/derby.
4094
+ - **`game/race`.** `raceTrack({ checkpoints, laps })` is an ordered ring of AABB checkpoint volumes (the final one is the finish line); `createRaceState({ track, win })` — driven each tick by `update(now, positions)` on game time — emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps split times, resolves a pluggable `RaceWinCondition` (`firstPastPost`, `topK` round-cut, `everyoneFinishes`, `lastStanding` derby), and `resetToCheckpoint(id)` hands back a respawn pose. `removeRacer(id)` drops a racer mid-race and renumbers the remaining standings; `reset()` clears all racer progress/finish state back to construction time so the same instance replays without rebuilding it. Trackmania, Mario Kart, Fall Guys.
4095
+
4096
+ ### Spawn placement
4097
+
4098
+ `spawn(catalogId, { id?, position | anchor, offset?, parentSpace?, group? })` — anchor `{ kind: "entity" | "zone", id }` with offset `{ radius, pattern }` or `{ xyz }`. Catalog supplies movement/model; no behaviors on spawn.
4099
+
4100
+ **Named spawn/respawn points** (`game/spawnPoints`) — `createSpawnPoints()`: `record(id, { x, y, z, rotationY? })` names a point (level bounds, team spawns, checkpoints), `get(id)`/`list()` read them back, `respawn(entities, entityId, spawnId)` teleports an existing entity to a recorded point via `setPose` in one call — the id-keyed alternative to threading raw coordinates through respawn logic by hand.
4101
+
4102
+ **Level sequence** (`game/levelSequence`) — `createLevelSequence({ levels: [{ id, config }], retriesPerLevel? })` is a pure, deterministic campaign machine: `start()` enters level 0, `clear()` marks the current level cleared, `advance()` moves to the next (or `"complete"` after the last), `fail()` consumes an attempt and returns `"retry"` while `retriesPerLevel` remain else `"failed"`, `retry()` restarts after a retry-eligible failure. `current()` → `{ id, index, config, attempt } | null`; `progress()` → `{ index, total, cleared }`; `reset()` rewinds to idle. Mirrors the reducer style of `game/race` and `ai/spawnDirector` — a level-select/roguelike-run campaign shell without hand-rolling the state machine per game.