@jgengine/core 0.18.1 → 0.21.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 (404) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/ai/decisionGraph.d.ts +50 -4
  3. package/dist/ai/decisionGraph.js +159 -23
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/ai/groupAssist.d.ts +10 -0
  7. package/dist/ai/groupAssist.js +11 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/threat.d.ts +8 -0
  11. package/dist/ai/threat.js +10 -0
  12. package/dist/anim/animGraph.d.ts +43 -1
  13. package/dist/anim/animGraph.js +326 -10
  14. package/dist/anim/boneTexture.d.ts +55 -0
  15. package/dist/anim/boneTexture.js +43 -0
  16. package/dist/anim/footPlacement.d.ts +57 -0
  17. package/dist/anim/footPlacement.js +67 -0
  18. package/dist/anim/locomotionGraph.d.ts +18 -1
  19. package/dist/anim/locomotionGraph.js +42 -3
  20. package/dist/audio/audioFalloff.d.ts +6 -0
  21. package/dist/audio/doppler.d.ts +21 -0
  22. package/dist/audio/doppler.js +32 -0
  23. package/dist/audio/engineLayers.d.ts +83 -0
  24. package/dist/audio/engineLayers.js +115 -0
  25. package/dist/audio/voiceAllocator.d.ts +46 -0
  26. package/dist/audio/voiceAllocator.js +76 -0
  27. package/dist/combat/abilityKit.d.ts +20 -0
  28. package/dist/combat/abilityKit.js +41 -4
  29. package/dist/combat/death.d.ts +11 -3
  30. package/dist/combat/death.js +3 -3
  31. package/dist/combat/magazine.d.ts +9 -0
  32. package/dist/combat/magazine.js +24 -2
  33. package/dist/combat/projectiles.d.ts +94 -2
  34. package/dist/combat/projectiles.js +282 -61
  35. package/dist/combat/regenShield.d.ts +14 -0
  36. package/dist/combat/regenShield.js +23 -7
  37. package/dist/combat/shotOrigin.d.ts +6 -0
  38. package/dist/combat/shotOrigin.js +6 -0
  39. package/dist/combat/weaponFeedback.d.ts +36 -0
  40. package/dist/combat/weaponFeedback.js +42 -0
  41. package/dist/combat/weaponHandling.d.ts +161 -0
  42. package/dist/combat/weaponHandling.js +202 -0
  43. package/dist/combat/weaponPresentation.d.ts +115 -0
  44. package/dist/combat/weaponPresentation.js +96 -0
  45. package/dist/crafting/recipeCatalog.d.ts +16 -0
  46. package/dist/crafting/recipeCatalog.js +55 -0
  47. package/dist/devtools/warnOnce.d.ts +8 -0
  48. package/dist/devtools/warnOnce.js +18 -0
  49. package/dist/economy/auctionBook.d.ts +1 -1
  50. package/dist/economy/auctionBook.js +11 -1
  51. package/dist/economy/listingBook.d.ts +1 -1
  52. package/dist/economy/listingBook.js +11 -1
  53. package/dist/economy/resourceLedger.js +3 -3
  54. package/dist/economy/shopStock.js +7 -6
  55. package/dist/economy/techTree.js +7 -6
  56. package/dist/economy/unlockPoints.js +3 -6
  57. package/dist/editor/commandApply.js +89 -16
  58. package/dist/editor/commands.d.ts +38 -1
  59. package/dist/editor/commands.js +320 -3
  60. package/dist/editor/creatorStorage.d.ts +59 -0
  61. package/dist/editor/creatorStorage.js +164 -0
  62. package/dist/editor/document.d.ts +4 -0
  63. package/dist/editor/document.js +119 -18
  64. package/dist/editor/environment.js +8 -0
  65. package/dist/editor/index.d.ts +2 -0
  66. package/dist/editor/liveSync.d.ts +4 -1
  67. package/dist/editor/liveSync.js +26 -10
  68. package/dist/editor/materialAuthoring.d.ts +14 -0
  69. package/dist/editor/materialAuthoring.js +38 -0
  70. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  71. package/dist/editor/modelNodeVisibility.js +29 -0
  72. package/dist/editor/movementCatalog.d.ts +90 -0
  73. package/dist/editor/movementCatalog.js +144 -0
  74. package/dist/editor/simulation.d.ts +60 -0
  75. package/dist/editor/simulation.js +202 -0
  76. package/dist/editor/staticPrefab.d.ts +17 -0
  77. package/dist/editor/staticPrefab.js +51 -0
  78. package/dist/editor/types.d.ts +15 -0
  79. package/dist/faction/reputation.d.ts +6 -0
  80. package/dist/faction/reputation.js +11 -0
  81. package/dist/game/cameraConfig.d.ts +54 -2
  82. package/dist/game/clipRoles.d.ts +2 -1
  83. package/dist/game/clipRoles.js +7 -3
  84. package/dist/game/contentValidation.d.ts +69 -0
  85. package/dist/game/contentValidation.js +118 -0
  86. package/dist/game/controlGate.d.ts +8 -0
  87. package/dist/game/controlGate.js +43 -6
  88. package/dist/game/defineGame.d.ts +32 -3
  89. package/dist/game/dialogueGraph.d.ts +20 -0
  90. package/dist/game/dialogueGraph.js +107 -0
  91. package/dist/game/events.d.ts +11 -0
  92. package/dist/game/keyValueStore.d.ts +17 -4
  93. package/dist/game/keyValueStore.js +35 -23
  94. package/dist/game/lootModifiers.d.ts +17 -0
  95. package/dist/game/lootModifiers.js +26 -0
  96. package/dist/game/lootTable.d.ts +4 -0
  97. package/dist/game/lootTable.js +16 -0
  98. package/dist/game/narrativeAuthoring.d.ts +67 -0
  99. package/dist/game/narrativeAuthoring.js +122 -0
  100. package/dist/game/playableGame.d.ts +134 -35
  101. package/dist/game/quest.d.ts +15 -0
  102. package/dist/game/quest.js +209 -63
  103. package/dist/game/questCatalog.d.ts +50 -0
  104. package/dist/game/questCatalog.js +205 -0
  105. package/dist/game/saveStore.d.ts +6 -5
  106. package/dist/game/saveStore.js +57 -57
  107. package/dist/game/systemRuntime.d.ts +2 -2
  108. package/dist/game/systemRuntime.js +84 -33
  109. package/dist/game/toasts.d.ts +8 -0
  110. package/dist/game/toasts.js +7 -0
  111. package/dist/game/vfxInstance.d.ts +9 -0
  112. package/dist/game/vfxInstance.js +12 -1
  113. package/dist/game/viewports.d.ts +43 -0
  114. package/dist/game/viewports.js +46 -0
  115. package/dist/game/worldItem.d.ts +5 -1
  116. package/dist/game/worldItem.js +0 -1
  117. package/dist/gameplay.d.ts +3 -0
  118. package/dist/gameplay.js +3 -0
  119. package/dist/input/actionBindings.d.ts +15 -1
  120. package/dist/input/actionBindings.js +14 -3
  121. package/dist/input/actionContexts.d.ts +25 -1
  122. package/dist/input/actionContexts.js +82 -25
  123. package/dist/input/axisInput.d.ts +4 -0
  124. package/dist/input/axisInput.js +4 -0
  125. package/dist/input/axisShaper.d.ts +73 -0
  126. package/dist/input/axisShaper.js +105 -0
  127. package/dist/input/gamepadModel.d.ts +36 -2
  128. package/dist/input/gamepadModel.js +69 -30
  129. package/dist/input/haptics.d.ts +50 -0
  130. package/dist/input/haptics.js +74 -0
  131. package/dist/input/inputBuffer.d.ts +40 -0
  132. package/dist/input/inputBuffer.js +59 -0
  133. package/dist/input/lookChannel.d.ts +10 -0
  134. package/dist/input/lookChannel.js +6 -0
  135. package/dist/input/pointer.d.ts +2 -0
  136. package/dist/input/pointer.js +4 -0
  137. package/dist/interaction/promptRegistry.d.ts +5 -4
  138. package/dist/interaction/promptRegistry.js +8 -2
  139. package/dist/inventory/inventoryModel.js +9 -6
  140. package/dist/inventory/shapedGrid.d.ts +48 -9
  141. package/dist/inventory/shapedGrid.js +69 -10
  142. package/dist/item/affix.d.ts +6 -0
  143. package/dist/item/affix.js +6 -0
  144. package/dist/item/durability.js +4 -0
  145. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  146. package/dist/item/itemInstanceRegistry.js +111 -7
  147. package/dist/item/itemgen.d.ts +1 -1
  148. package/dist/item/itemgen.js +1 -1
  149. package/dist/item/modularItem.js +7 -1
  150. package/dist/material/appearanceSignals.d.ts +53 -0
  151. package/dist/material/appearanceSignals.js +47 -0
  152. package/dist/material/hairCards.d.ts +47 -0
  153. package/dist/material/hairCards.js +136 -0
  154. package/dist/material/materialAsset.d.ts +162 -0
  155. package/dist/material/materialAsset.js +382 -0
  156. package/dist/meta/changelog.d.ts +1 -1
  157. package/dist/meta/changelog.js +444 -1
  158. package/dist/movement/characterController.d.ts +5 -2
  159. package/dist/movement/characterController.js +50 -23
  160. package/dist/movement/freeFlight.d.ts +2 -2
  161. package/dist/movement/freeFlight.js +3 -3
  162. package/dist/movement/glideModel.d.ts +3 -0
  163. package/dist/movement/glideModel.js +6 -0
  164. package/dist/movement/grappleSwing.d.ts +2 -0
  165. package/dist/movement/grappleSwing.js +5 -1
  166. package/dist/movement/movementModel.d.ts +63 -1
  167. package/dist/movement/movementModel.js +136 -26
  168. package/dist/movement/movementProbe.d.ts +51 -0
  169. package/dist/movement/movementProbe.js +119 -0
  170. package/dist/movement/playerMovement.d.ts +48 -2
  171. package/dist/movement/playerMovement.js +493 -87
  172. package/dist/movement/poseState.d.ts +2 -0
  173. package/dist/movement/poseState.js +29 -1
  174. package/dist/movement/solidObstacles.d.ts +11 -1
  175. package/dist/movement/solidObstacles.js +48 -3
  176. package/dist/movement/terrainGrade.d.ts +13 -0
  177. package/dist/movement/terrainGrade.js +33 -0
  178. package/dist/movement/voxelController.d.ts +2 -2
  179. package/dist/movement/voxelController.js +10 -10
  180. package/dist/nav/navFromEnvironment.d.ts +9 -3
  181. package/dist/nav/navFromEnvironment.js +18 -12
  182. package/dist/nav/navGrid.d.ts +19 -1
  183. package/dist/nav/navGrid.js +26 -10
  184. package/dist/nav/navMesh.d.ts +43 -2
  185. package/dist/nav/navMesh.js +534 -128
  186. package/dist/nav/solidRoute.d.ts +50 -0
  187. package/dist/nav/solidRoute.js +144 -0
  188. package/dist/physics/aircraftDynamics.d.ts +390 -0
  189. package/dist/physics/aircraftDynamics.js +704 -0
  190. package/dist/physics/ballisticSweep.d.ts +10 -3
  191. package/dist/physics/ballisticSweep.js +53 -29
  192. package/dist/physics/boatDynamics.d.ts +116 -0
  193. package/dist/physics/boatDynamics.js +160 -0
  194. package/dist/physics/drivableVehicle.d.ts +22 -8
  195. package/dist/physics/drivableVehicle.js +4 -2
  196. package/dist/physics/flightDynamics.d.ts +21 -4
  197. package/dist/physics/flightDynamics.js +28 -5
  198. package/dist/physics/forceVolume.d.ts +41 -0
  199. package/dist/physics/forceVolume.js +88 -0
  200. package/dist/physics/handlingProbe.d.ts +249 -0
  201. package/dist/physics/handlingProbe.js +606 -0
  202. package/dist/physics/kinematicVehicle.d.ts +40 -5
  203. package/dist/physics/kinematicVehicle.js +17 -2
  204. package/dist/physics/physicsBackend.d.ts +12 -0
  205. package/dist/physics/physicsBackendConformance.js +34 -0
  206. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  207. package/dist/physics/physicsWorldBackend.js +52 -3
  208. package/dist/physics/spatialGrid.d.ts +1 -1
  209. package/dist/physics/spatialGrid.js +8 -9
  210. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  211. package/dist/physics/vehicleBackendLink.js +102 -0
  212. package/dist/physics/vehicleDynamics.d.ts +375 -0
  213. package/dist/physics/vehicleDynamics.js +737 -0
  214. package/dist/physics/vehicleObstacles.d.ts +3 -2
  215. package/dist/physics/vehicleObstacles.js +29 -5
  216. package/dist/physics/worldColliders.d.ts +1 -1
  217. package/dist/physics/worldColliders.js +35 -3
  218. package/dist/puzzle/cellGrid.d.ts +15 -9
  219. package/dist/puzzle/cellGrid.js +13 -9
  220. package/dist/render/lookPreset.d.ts +9 -2
  221. package/dist/render/lookPreset.js +29 -2
  222. package/dist/render/postProcessing.d.ts +30 -0
  223. package/dist/render/postProcessing.js +1 -1
  224. package/dist/runtime/cameraDirector.d.ts +54 -3
  225. package/dist/runtime/cameraDirector.js +187 -0
  226. package/dist/runtime/commandDispatch.d.ts +14 -0
  227. package/dist/runtime/commandDispatch.js +19 -0
  228. package/dist/runtime/commandInput.d.ts +37 -0
  229. package/dist/runtime/commandInput.js +58 -0
  230. package/dist/runtime/commandRunner.d.ts +27 -0
  231. package/dist/runtime/commandRunner.js +31 -3
  232. package/dist/runtime/context/combat.d.ts +12 -4
  233. package/dist/runtime/context/combat.js +179 -11
  234. package/dist/runtime/context/deathLoot.d.ts +11 -8
  235. package/dist/runtime/context/deathLoot.js +17 -16
  236. package/dist/runtime/context/scene.d.ts +2 -0
  237. package/dist/runtime/context/scene.js +13 -21
  238. package/dist/runtime/descriptors/baseline.js +34 -3
  239. package/dist/runtime/descriptors/features.d.ts +2 -1
  240. package/dist/runtime/descriptors/features.js +18 -0
  241. package/dist/runtime/gameContext.js +86 -13
  242. package/dist/runtime/gameContextTypes.d.ts +33 -3
  243. package/dist/runtime/gameRuntime.d.ts +3 -1
  244. package/dist/runtime/gameRuntime.js +4 -0
  245. package/dist/runtime/headlessRunner.d.ts +3 -1
  246. package/dist/runtime/headlessRunner.js +11 -11
  247. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  248. package/dist/runtime/hostedGameRunner.js +156 -10
  249. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  250. package/dist/runtime/hostedWorldSession.js +154 -16
  251. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  252. package/dist/runtime/inputRecorder.d.ts +1 -1
  253. package/dist/runtime/inputRecorder.js +21 -1
  254. package/dist/runtime/inputSnapshot.d.ts +22 -4
  255. package/dist/runtime/inputSnapshot.js +41 -3
  256. package/dist/runtime/localPlayers.d.ts +73 -0
  257. package/dist/runtime/localPlayers.js +119 -0
  258. package/dist/runtime/runtimeSave.d.ts +10 -6
  259. package/dist/runtime/runtimeSave.js +11 -5
  260. package/dist/runtime/simContext.d.ts +8 -3
  261. package/dist/runtime/simContext.js +7 -3
  262. package/dist/runtime/simSnapshot.js +2 -0
  263. package/dist/runtime/transport.d.ts +9 -0
  264. package/dist/runtime/transport.js +7 -1
  265. package/dist/runtime/worldReplication.d.ts +2 -0
  266. package/dist/runtime/worldReplication.js +1 -1
  267. package/dist/scene/assetCatalog.d.ts +2 -0
  268. package/dist/scene/behaviorRuntime.d.ts +38 -7
  269. package/dist/scene/behaviorRuntime.js +293 -30
  270. package/dist/scene/behaviors.d.ts +35 -2
  271. package/dist/scene/behaviors.js +29 -0
  272. package/dist/scene/entityStore.d.ts +14 -3
  273. package/dist/scene/entityStore.js +45 -9
  274. package/dist/scene/mount.d.ts +9 -0
  275. package/dist/scene/mount.js +35 -0
  276. package/dist/scene/possession.d.ts +2 -0
  277. package/dist/scene/possession.js +21 -1
  278. package/dist/scene/sceneKinds.d.ts +9 -0
  279. package/dist/scene/sceneRaycast.d.ts +7 -0
  280. package/dist/scene/sceneRaycast.js +55 -0
  281. package/dist/scene/spatial.d.ts +20 -1
  282. package/dist/scene/spatial.js +94 -2
  283. package/dist/scene/vehicleSeat.d.ts +41 -3
  284. package/dist/scene/vehicleSeat.js +94 -3
  285. package/dist/sensor/concealment.d.ts +6 -0
  286. package/dist/sensor/concealment.js +8 -0
  287. package/dist/sensor/freezeMonitor.d.ts +6 -0
  288. package/dist/sensor/freezeMonitor.js +8 -0
  289. package/dist/sensor/perception.d.ts +11 -1
  290. package/dist/sensor/perception.js +57 -10
  291. package/dist/sensor/recordingBuffer.d.ts +3 -0
  292. package/dist/sensor/recordingBuffer.js +8 -0
  293. package/dist/session/roundState.d.ts +4 -0
  294. package/dist/session/roundState.js +11 -0
  295. package/dist/settings/frameRateLimit.d.ts +16 -0
  296. package/dist/settings/frameRateLimit.js +37 -0
  297. package/dist/settings/graphicsProfile.d.ts +2 -0
  298. package/dist/settings/graphicsProfile.js +12 -0
  299. package/dist/settings/graphicsSettings.d.ts +40 -0
  300. package/dist/settings/graphicsSettings.js +81 -0
  301. package/dist/settings/settingsModel.d.ts +9 -1
  302. package/dist/settings/settingsModel.js +10 -0
  303. package/dist/stats/accumulatorMeter.d.ts +10 -0
  304. package/dist/stats/accumulatorMeter.js +6 -0
  305. package/dist/stats/eventMeter.d.ts +10 -3
  306. package/dist/stats/eventMeter.js +6 -3
  307. package/dist/store/defineStore.d.ts +3 -1
  308. package/dist/store/defineStore.js +3 -1
  309. package/dist/store/observableKeyedStore.d.ts +1 -1
  310. package/dist/store/observableKeyedStore.js +8 -6
  311. package/dist/time/simClock.d.ts +6 -2
  312. package/dist/time/simClock.js +5 -0
  313. package/dist/vfx/feedbackMixer.d.ts +61 -0
  314. package/dist/vfx/feedbackMixer.js +106 -0
  315. package/dist/vfx/particleDirector.d.ts +58 -6
  316. package/dist/vfx/particleDirector.js +113 -22
  317. package/dist/vfx/particles.d.ts +68 -5
  318. package/dist/vfx/particles.js +284 -32
  319. package/dist/visibility/assetStreaming.d.ts +21 -3
  320. package/dist/visibility/assetStreaming.js +108 -24
  321. package/dist/visibility/settings.d.ts +8 -2
  322. package/dist/visibility/settings.js +3 -0
  323. package/dist/visibility/simulationCulling.d.ts +8 -1
  324. package/dist/visibility/simulationCulling.js +7 -0
  325. package/dist/visibility/spatialIndex.d.ts +21 -0
  326. package/dist/visibility/spatialIndex.js +16 -0
  327. package/dist/visibility/visibilitySystem.js +1 -0
  328. package/dist/work/expedition.d.ts +110 -0
  329. package/dist/work/expedition.js +195 -0
  330. package/dist/work/staffedStation.d.ts +29 -0
  331. package/dist/work/staffedStation.js +27 -0
  332. package/dist/world/authoredAnimation.d.ts +38 -0
  333. package/dist/world/authoredAnimation.js +99 -0
  334. package/dist/world/authoredObjects.d.ts +24 -4
  335. package/dist/world/authoredObjects.js +43 -16
  336. package/dist/world/authoredSimulation.d.ts +74 -0
  337. package/dist/world/authoredSimulation.js +302 -0
  338. package/dist/world/authoredSolids.d.ts +46 -0
  339. package/dist/world/authoredSolids.js +75 -0
  340. package/dist/world/authoredWeather.d.ts +120 -0
  341. package/dist/world/authoredWeather.js +314 -0
  342. package/dist/world/buildingGenerator.js +2 -2
  343. package/dist/world/buildingKit.d.ts +11 -4
  344. package/dist/world/buildingKit.js +21 -8
  345. package/dist/world/buildings.d.ts +34 -2
  346. package/dist/world/buildings.js +11 -1
  347. package/dist/world/cityKind.d.ts +7 -0
  348. package/dist/world/cityKind.js +52 -1
  349. package/dist/world/environmentForces.d.ts +21 -0
  350. package/dist/world/environmentForces.js +42 -0
  351. package/dist/world/features.d.ts +32 -5
  352. package/dist/world/features.js +6 -0
  353. package/dist/world/footprintGrid.d.ts +62 -0
  354. package/dist/world/footprintGrid.js +217 -0
  355. package/dist/world/interiors.d.ts +1 -0
  356. package/dist/world/interiors.js +5 -3
  357. package/dist/world/lod.d.ts +6 -0
  358. package/dist/world/lod.js +8 -0
  359. package/dist/world/pathTerrain.d.ts +1 -1
  360. package/dist/world/terraform.d.ts +9 -1
  361. package/dist/world/terraform.js +53 -18
  362. package/dist/world/walls.d.ts +7 -0
  363. package/dist/world/walls.js +7 -0
  364. package/dist/world/weather.d.ts +17 -0
  365. package/dist/world/weather.js +116 -20
  366. package/dist/world/worldSolids.d.ts +95 -0
  367. package/dist/world/worldSolids.js +351 -0
  368. package/dist/world.d.ts +6 -6
  369. package/dist/world.js +5 -5
  370. package/package.json +1 -1
  371. package/skills/ce-handoff/SKILL.md +2 -2
  372. package/skills/game-design/SKILL.md +2 -2
  373. package/skills/jgengine/SKILL.md +28 -6
  374. package/skills/jgengine/api.md +139 -99
  375. package/skills/jgengine/capabilities.md +28 -0
  376. package/skills/jgengine/recipes/minimal-game.md +10 -2
  377. package/skills/jgengine-combat/SKILL.md +13 -2
  378. package/skills/jgengine-combat/api.md +150 -103
  379. package/skills/jgengine-combat/capabilities.md +29 -1
  380. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  381. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  382. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  383. package/skills/jgengine-combat/reference.md +24 -2
  384. package/skills/jgengine-gameplay/SKILL.md +63 -0
  385. package/skills/jgengine-gameplay/api.md +583 -463
  386. package/skills/jgengine-gameplay/capabilities.md +92 -0
  387. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  388. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  389. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  390. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  391. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  392. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  393. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  394. package/skills/jgengine-gameplay/reference-systems.md +123 -0
  395. package/skills/jgengine-world/SKILL.md +25 -4
  396. package/skills/jgengine-world/api.md +1045 -655
  397. package/skills/jgengine-world/capabilities.md +246 -9
  398. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  399. package/skills/jgengine-world/recipes/controls.md +48 -0
  400. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  401. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  402. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  403. package/skills/jgengine-world/reference.md +180 -4
  404. package/skills/jgengine-world/visibility.md +139 -0
@@ -1,3 +1,4 @@
1
+ import type { ModelAnimationConfig } from "../game/playableGame.js";
1
2
  import type { AnimGraph } from "./animGraph.js";
2
3
  /** Inputs for {@link locomotionGraph}: the idle/walk/run clip names and the one-shot table a rig config already carries. */
3
4
  export interface LocomotionGraphInput {
@@ -11,10 +12,16 @@ export interface LocomotionGraphInput {
11
12
  /** Crossfade seconds between locomotion states. Default 0.2. */
12
13
  fadeSec?: number;
13
14
  /** One-shot clips keyed by trigger name; `death` holds its last frame instead of returning. */
14
- oneShots?: Readonly<Record<string, string>>;
15
+ oneShots?: Readonly<Record<string, string | readonly string[]>>;
15
16
  }
16
17
  /** The parameter name the shell feeds with the entity's smoothed ground speed. */
17
18
  export declare const LOCOMOTION_SPEED_PARAM = "speed";
19
+ /** Ground contact reported by the shared player motor; absent for custom movers without authored parameters. */
20
+ export declare const LOCOMOTION_GROUNDED_PARAM = "grounded";
21
+ /** Resolved vertical velocity from the shared player motor, in world units/sec. */
22
+ export declare const LOCOMOTION_VERTICAL_SPEED_PARAM = "verticalSpeed";
23
+ /** Physically resolved crouch stance from the shared player motor. */
24
+ export declare const LOCOMOTION_CROUCHED_PARAM = "crouched";
18
25
  /** Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`. */
19
26
  export declare const LOCOMOTION_LAYER = "base";
20
27
  /**
@@ -24,3 +31,13 @@ export declare const LOCOMOTION_LAYER = "base";
24
31
  * @capability locomotion-graph build the default idle/walk/run plus one-shot graph from clip names
25
32
  */
26
33
  export declare function locomotionGraph(input: LocomotionGraphInput): AnimGraph;
34
+ /**
35
+ * The graph a model animation config plays: its `graph`, or the {@link locomotionGraph} its
36
+ * `states` and `oneShots` describe, retaining every one-shot variant. `undefined` for a
37
+ * single-clip config or incomplete locomotion roles. Idle must name a clip; an absent or blank
38
+ * walk role holds that idle clip at walking speeds until configured. The shell plays this and
39
+ * the editor inspects it, so both see the same graph.
40
+ *
41
+ * @capability locomotion-graph resolve the animation graph a model config plays
42
+ */
43
+ export declare function animGraphFromConfig(config: ModelAnimationConfig): AnimGraph | undefined;
@@ -1,5 +1,11 @@
1
1
  /** The parameter name the shell feeds with the entity's smoothed ground speed. */
2
2
  export const LOCOMOTION_SPEED_PARAM = "speed";
3
+ /** Ground contact reported by the shared player motor; absent for custom movers without authored parameters. */
4
+ export const LOCOMOTION_GROUNDED_PARAM = "grounded";
5
+ /** Resolved vertical velocity from the shared player motor, in world units/sec. */
6
+ export const LOCOMOTION_VERTICAL_SPEED_PARAM = "verticalSpeed";
7
+ /** Physically resolved crouch stance from the shared player motor. */
8
+ export const LOCOMOTION_CROUCHED_PARAM = "crouched";
3
9
  /** Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`. */
4
10
  export const LOCOMOTION_LAYER = "base";
5
11
  /**
@@ -20,10 +26,18 @@ export function locomotionGraph(input) {
20
26
  const states = {
21
27
  locomotion: { kind: "blend1D", param: LOCOMOTION_SPEED_PARAM, points },
22
28
  };
29
+ const oneShots = Object.entries(input.oneShots ?? {}).filter(([, spec]) => typeof spec === "string" || spec.length > 0);
30
+ for (const [name, spec] of oneShots) {
31
+ const clip = typeof spec === "string" ? spec : spec[0];
32
+ states[name] = { kind: "clip", clip, loop: false, ...(typeof spec === "string" || spec.length < 2 ? {} : { variants: [...spec] }) };
33
+ }
23
34
  const transitions = [];
24
- for (const [name, clip] of Object.entries(input.oneShots ?? {})) {
25
- states[name] = { kind: "clip", clip, loop: false };
26
- transitions.push({ from: "*", to: name, trigger: name, duration: 0.1 });
35
+ oneShots.sort(([a], [b]) => Number(b === "death") - Number(a === "death"));
36
+ for (const [name] of oneShots) {
37
+ for (const from of Object.keys(states)) {
38
+ if (from !== "death")
39
+ transitions.push({ from, to: name, trigger: name, duration: 0.1 });
40
+ }
27
41
  if (name !== "death")
28
42
  transitions.push({ from: name, to: "locomotion", exitTime: 1, duration: fade });
29
43
  }
@@ -31,3 +45,28 @@ export function locomotionGraph(input) {
31
45
  layers: [{ id: LOCOMOTION_LAYER, entry: "locomotion", states, transitions }],
32
46
  };
33
47
  }
48
+ /**
49
+ * The graph a model animation config plays: its `graph`, or the {@link locomotionGraph} its
50
+ * `states` and `oneShots` describe, retaining every one-shot variant. `undefined` for a
51
+ * single-clip config or incomplete locomotion roles. Idle must name a clip; an absent or blank
52
+ * walk role holds that idle clip at walking speeds until configured. The shell plays this and
53
+ * the editor inspects it, so both see the same graph.
54
+ *
55
+ * @capability locomotion-graph resolve the animation graph a model config plays
56
+ */
57
+ export function animGraphFromConfig(config) {
58
+ if (config.graph !== undefined)
59
+ return config.graph;
60
+ const states = config.states;
61
+ if (states === undefined || typeof states.idle !== "string" || states.idle.trim().length === 0)
62
+ return undefined;
63
+ return locomotionGraph({
64
+ idle: states.idle,
65
+ walk: typeof states.walk === "string" && states.walk.trim().length > 0 ? states.walk : states.idle,
66
+ ...(typeof states.run !== "string" || states.run.trim().length === 0 ? {} : { run: states.run }),
67
+ walkSpeed: states.walkSpeed,
68
+ runSpeed: states.runSpeed,
69
+ fadeSec: states.fadeSec,
70
+ ...(config.oneShots === undefined ? {} : { oneShots: config.oneShots }),
71
+ });
72
+ }
@@ -24,9 +24,15 @@ export interface SoundDef {
24
24
  /** Base gain before bus/falloff multipliers. Default 1. */
25
25
  gain?: number;
26
26
  loop?: boolean;
27
+ /** Maximum simultaneous reservations for this sound, including pending loads. Default the engine's total voice limit. */
28
+ maxVoices?: number;
29
+ /** Playback priority; higher priorities survive voice-budget pressure. Default 0. */
30
+ priority?: number;
27
31
  /** Positional emitters attenuate by distance from the listener; non-positional sounds (UI, music) play at flat gain. Default true. */
28
32
  positional?: boolean;
29
33
  falloff?: AudioFalloffConfig;
34
+ /** Doppler strength for retained loops: 1 is physical, 0 or absent is off. Emitter velocity comes from `setLoop({ velocity })`, listener velocity from the camera. */
35
+ doppler?: number;
30
36
  /** Web Audio spatial panning configuration. When present, the shell uses a PannerNode. */
31
37
  spatial?: {
32
38
  panning: "hrtf" | "equalpower";
@@ -0,0 +1,21 @@
1
+ /** Speed of sound in air at 20 °C, metres per second — the default for {@link dopplerRate}. */
2
+ export declare const SPEED_OF_SOUND = 343;
3
+ type Vec3Tuple = readonly [number, number, number];
4
+ /** Options for {@link dopplerRate}. */
5
+ export interface DopplerOptions {
6
+ /** Scales both velocities; 1 is physical, 0 disables the shift. Default 1. */
7
+ factor?: number;
8
+ /** World units per second sound travels. Default {@link SPEED_OF_SOUND}. */
9
+ speedOfSound?: number;
10
+ /** Clamp window for the returned multiplier. Default `[0.5, 2]`. */
11
+ range?: readonly [number, number];
12
+ }
13
+ /**
14
+ * Doppler pitch multiplier for an emitter heard by a listener: `(c + vListener) / (c - vEmitter)`,
15
+ * where each speed is the component along the line between them, positive when closing. Multiply a
16
+ * loop's playback rate by it. Pure and allocation-free.
17
+ *
18
+ * @capability doppler-pitch pitch a moving sound up as it approaches and down as it passes, from listener and emitter velocity
19
+ */
20
+ export declare function dopplerRate(listenerPosition: Vec3Tuple, listenerVelocity: Vec3Tuple, emitterPosition: Vec3Tuple, emitterVelocity: Vec3Tuple, options?: DopplerOptions): number;
21
+ export {};
@@ -0,0 +1,32 @@
1
+ /** Speed of sound in air at 20 °C, metres per second — the default for {@link dopplerRate}. */
2
+ export const SPEED_OF_SOUND = 343;
3
+ /**
4
+ * Doppler pitch multiplier for an emitter heard by a listener: `(c + vListener) / (c - vEmitter)`,
5
+ * where each speed is the component along the line between them, positive when closing. Multiply a
6
+ * loop's playback rate by it. Pure and allocation-free.
7
+ *
8
+ * @capability doppler-pitch pitch a moving sound up as it approaches and down as it passes, from listener and emitter velocity
9
+ */
10
+ export function dopplerRate(listenerPosition, listenerVelocity, emitterPosition, emitterVelocity, options = {}) {
11
+ const factor = options.factor ?? 1;
12
+ const c = options.speedOfSound ?? SPEED_OF_SOUND;
13
+ const min = options.range?.[0] ?? 0.5;
14
+ const max = options.range?.[1] ?? 2;
15
+ const dx = emitterPosition[0] - listenerPosition[0];
16
+ const dy = emitterPosition[1] - listenerPosition[1];
17
+ const dz = emitterPosition[2] - listenerPosition[2];
18
+ const distance = Math.hypot(dx, dy, dz);
19
+ if (factor === 0 || c <= 0 || distance < 1e-6)
20
+ return 1;
21
+ const ux = dx / distance;
22
+ const uy = dy / distance;
23
+ const uz = dz / distance;
24
+ const listenerClosing = factor * (listenerVelocity[0] * ux + listenerVelocity[1] * uy + listenerVelocity[2] * uz);
25
+ const emitterClosing = -factor * (emitterVelocity[0] * ux + emitterVelocity[1] * uy + emitterVelocity[2] * uz);
26
+ const numerator = c + Math.max(-c * 0.99, listenerClosing);
27
+ const denominator = c - Math.min(c * 0.99, emitterClosing);
28
+ const rate = numerator / denominator;
29
+ if (!Number.isFinite(rate))
30
+ return 1;
31
+ return rate < min ? min : rate > max ? max : rate;
32
+ }
@@ -0,0 +1,83 @@
1
+ /** One recorded engine sample, keyed by the rpm it was recorded at. */
2
+ export interface EngineLayer {
3
+ /** Catalog sound id of the loop. */
4
+ sound: string;
5
+ /** Rpm the sample was recorded at; playback rate is `rpm / layer.rpm`. */
6
+ rpm: number;
7
+ /** Which load set the layer belongs to: `"on"` (throttle), `"off"` (coast/overrun) or `"any"` (always). Default `"any"`. */
8
+ load?: "on" | "off" | "any";
9
+ /** Extra gain for this layer. Default 1. */
10
+ gain?: number;
11
+ }
12
+ /** Config for {@link createEngineLayers}. */
13
+ export interface EngineLayersConfig {
14
+ /** Prefix for the retained loop ids, one per layer: `${id}:${index}`. */
15
+ id: string;
16
+ layers: readonly EngineLayer[];
17
+ /** Load window the off-load set crossfades to the on-load set across. Default `[0.15, 0.6]`. */
18
+ loadCrossfade?: readonly [number, number];
19
+ /** Exponential rate the load follows `engineLoad` with, so a lifted throttle does not click between sets. Default 10. */
20
+ loadResponse?: number;
21
+ }
22
+ /** One layer's live mix. */
23
+ export interface EngineLayerMix {
24
+ /** Retained loop id. */
25
+ id: string;
26
+ sound: string;
27
+ gain: number;
28
+ rate: number;
29
+ }
30
+ /** Serializable {@link EngineLayers} state. */
31
+ export interface EngineLayersState {
32
+ load: number;
33
+ }
34
+ /** Per-tick parameters {@link EngineLayers.play} forwards to every layer's loop. */
35
+ export interface EngineLayersPlayOptions {
36
+ at?: readonly [number, number, number];
37
+ velocity?: readonly [number, number, number];
38
+ /** Overall gain multiplier. Default 1. */
39
+ gain?: number;
40
+ lowpass?: number;
41
+ highpass?: number;
42
+ }
43
+ /** The part of `ctx.game.audio` {@link EngineLayers} drives. */
44
+ export interface EngineLayersAudio {
45
+ loop(id: string, sound: string, options?: {
46
+ at?: readonly [number, number, number];
47
+ }): void;
48
+ setLoop(id: string, params: {
49
+ rate?: number;
50
+ gain?: number;
51
+ at?: readonly [number, number, number];
52
+ velocity?: readonly [number, number, number];
53
+ lowpass?: number;
54
+ highpass?: number;
55
+ }): void;
56
+ stopLoop(id: string): void;
57
+ }
58
+ /** A layered engine loop: N rpm-keyed samples crossfaded by rpm, with on- and off-load sets picked by engine load. */
59
+ export interface EngineLayers {
60
+ /** Advance by `dt` and return each layer's gain and rate. The returned array and its entries are reused between calls. */
61
+ update(dt: number, signals: {
62
+ rpm: number;
63
+ load: number;
64
+ }): readonly EngineLayerMix[];
65
+ /** Start (idempotently) and update every layer's retained loop from the last {@link update}. */
66
+ play(audio: EngineLayersAudio, options?: EngineLayersPlayOptions): void;
67
+ /** Stop every layer's loop. */
68
+ stop(audio: EngineLayersAudio): void;
69
+ mix(): readonly EngineLayerMix[];
70
+ retune(next: EngineLayersConfig): void;
71
+ snapshot(): EngineLayersState;
72
+ restore(state: EngineLayersState): void;
73
+ reset(): void;
74
+ }
75
+ /**
76
+ * Layered engine sound: each set of rpm-keyed samples is equal-power crossfaded between the two
77
+ * samples bracketing the current rpm and pitched by `rpm / layer.rpm`; the on- and off-load sets are
78
+ * equal-power crossfaded by smoothed `engineLoad`. A config with only one load set plays it at every
79
+ * load. `play` drives `ctx.game.audio` directly, so a game needs no per-layer glue.
80
+ *
81
+ * @capability engine-layers layered engine audio: rpm-keyed samples crossfaded, with on-load and off-load sets picked by engine load
82
+ */
83
+ export declare function createEngineLayers(initial: EngineLayersConfig): EngineLayers;
@@ -0,0 +1,115 @@
1
+ function indexLayers(layers) {
2
+ const byRpm = (a, b) => layers[a].rpm - layers[b].rpm;
3
+ const index = { on: [], off: [], any: [] };
4
+ layers.forEach((layer, i) => index[layer.load ?? "any"].push(i));
5
+ if (index.on.length === 0 || index.off.length === 0) {
6
+ index.any.push(...index.on, ...index.off);
7
+ index.on = [];
8
+ index.off = [];
9
+ }
10
+ index.on.sort(byRpm);
11
+ index.off.sort(byRpm);
12
+ index.any.sort(byRpm);
13
+ return index;
14
+ }
15
+ function smoothstep(edge0, edge1, x) {
16
+ if (edge1 <= edge0)
17
+ return x >= edge1 ? 1 : 0;
18
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
19
+ return t * t * (3 - 2 * t);
20
+ }
21
+ /**
22
+ * Layered engine sound: each set of rpm-keyed samples is equal-power crossfaded between the two
23
+ * samples bracketing the current rpm and pitched by `rpm / layer.rpm`; the on- and off-load sets are
24
+ * equal-power crossfaded by smoothed `engineLoad`. A config with only one load set plays it at every
25
+ * load. `play` drives `ctx.game.audio` directly, so a game needs no per-layer glue.
26
+ *
27
+ * @capability engine-layers layered engine audio: rpm-keyed samples crossfaded, with on-load and off-load sets picked by engine load
28
+ */
29
+ export function createEngineLayers(initial) {
30
+ let config = initial;
31
+ let index = indexLayers(config.layers);
32
+ let mixes = [];
33
+ let load = 0;
34
+ const params = {};
35
+ function rebuild() {
36
+ index = indexLayers(config.layers);
37
+ mixes = config.layers.map((layer, i) => ({ id: `${config.id}:${i}`, sound: layer.sound, gain: 0, rate: 1 }));
38
+ }
39
+ rebuild();
40
+ function crossfade(set, rpm, weight) {
41
+ if (set.length === 0 || weight <= 0)
42
+ return;
43
+ const layers = config.layers;
44
+ let upper = 0;
45
+ while (upper < set.length && layers[set[upper]].rpm < rpm)
46
+ upper += 1;
47
+ if (upper === 0 || upper === set.length) {
48
+ const only = set[upper === 0 ? 0 : set.length - 1];
49
+ mixes[only].gain += weight;
50
+ return;
51
+ }
52
+ const lowIndex = set[upper - 1];
53
+ const highIndex = set[upper];
54
+ const low = layers[lowIndex].rpm;
55
+ const high = layers[highIndex].rpm;
56
+ const t = high > low ? (rpm - low) / (high - low) : 1;
57
+ mixes[lowIndex].gain += weight * Math.cos((t * Math.PI) / 2);
58
+ mixes[highIndex].gain += weight * Math.sin((t * Math.PI) / 2);
59
+ }
60
+ return {
61
+ update(dt, signals) {
62
+ const rpm = Number.isFinite(signals.rpm) ? Math.max(0, signals.rpm) : 0;
63
+ const target = Number.isFinite(signals.load) ? Math.min(1, Math.max(0, signals.load)) : 0;
64
+ const response = config.loadResponse ?? 10;
65
+ load = Number.isFinite(response) ? load + (target - load) * (1 - Math.exp(-response * Math.max(0, dt))) : target;
66
+ const [from, to] = config.loadCrossfade ?? [0.15, 0.6];
67
+ const onWeight = smoothstep(from, to, load);
68
+ for (let i = 0; i < mixes.length; i += 1) {
69
+ const layer = config.layers[i];
70
+ mixes[i].gain = 0;
71
+ mixes[i].rate = layer.rpm > 0 ? rpm / layer.rpm : 1;
72
+ }
73
+ crossfade(index.any, rpm, 1);
74
+ crossfade(index.on, rpm, Math.sin((onWeight * Math.PI) / 2));
75
+ crossfade(index.off, rpm, Math.cos((onWeight * Math.PI) / 2));
76
+ for (let i = 0; i < mixes.length; i += 1)
77
+ mixes[i].gain *= config.layers[i].gain ?? 1;
78
+ return mixes;
79
+ },
80
+ play(audio, options = {}) {
81
+ const scale = options.gain ?? 1;
82
+ const loopOptions = options.at === undefined ? undefined : { at: options.at };
83
+ params.at = options.at;
84
+ params.velocity = options.velocity;
85
+ params.lowpass = options.lowpass;
86
+ params.highpass = options.highpass;
87
+ for (const mix of mixes) {
88
+ audio.loop(mix.id, mix.sound, loopOptions);
89
+ params.rate = mix.rate;
90
+ params.gain = mix.gain * scale;
91
+ audio.setLoop(mix.id, params);
92
+ }
93
+ },
94
+ stop(audio) {
95
+ for (const mix of mixes)
96
+ audio.stopLoop(mix.id);
97
+ },
98
+ mix: () => mixes,
99
+ retune(next) {
100
+ config = next;
101
+ rebuild();
102
+ },
103
+ snapshot: () => ({ load }),
104
+ restore(state) {
105
+ load = state.load;
106
+ },
107
+ reset() {
108
+ load = 0;
109
+ for (const mix of mixes) {
110
+ mix.gain = 0;
111
+ mix.rate = 1;
112
+ }
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,46 @@
1
+ /** One reserved playback slot, including sources still loading. */
2
+ export interface AudioVoice {
3
+ id: number;
4
+ soundId: string;
5
+ priority: number;
6
+ }
7
+ /** Detached allocator state; ids increase monotonically for deterministic oldest-first ties. */
8
+ export interface VoiceAllocatorSnapshot {
9
+ nextId: number;
10
+ voices: AudioVoice[];
11
+ }
12
+ /** Caller-owned reservation storage. Reads and writes use detached state. */
13
+ export interface VoiceAllocatorStorage {
14
+ read(): VoiceAllocatorSnapshot;
15
+ write(state: VoiceAllocatorSnapshot): void;
16
+ }
17
+ /** Playback budget. Zero disables playback; default 64. */
18
+ export interface VoiceAllocatorConfig {
19
+ maxTotal?: number;
20
+ /** At capacity, steal the lowest-priority oldest voice, or reject new voices. Default "steal-lowest". */
21
+ overflow?: "steal-lowest" | "reject";
22
+ storage?: VoiceAllocatorStorage;
23
+ }
24
+ /** Admission result; stop every stolen source before starting the admitted voice. */
25
+ export type VoiceAllocation = {
26
+ ok: false;
27
+ } | {
28
+ ok: true;
29
+ voiceId: number;
30
+ stolen: number[];
31
+ };
32
+ /** Bounded reservations with higher-priority protection and oldest-first ties. */
33
+ export interface VoiceAllocator {
34
+ request(soundId: string, priority?: number, maxVoices?: number): VoiceAllocation;
35
+ release(voiceId: number): void;
36
+ snapshot(): VoiceAllocatorSnapshot;
37
+ restore(state: VoiceAllocatorSnapshot): void;
38
+ /** Return reservations removed to meet the new limit; the playback owner stops them. */
39
+ retune(config: Pick<VoiceAllocatorConfig, "maxTotal" | "overflow">): number[];
40
+ }
41
+ /**
42
+ * Reserve bounded sound playback before fetching or constructing a graph. Equal priorities steal
43
+ * the oldest reservation; lower priorities cannot interrupt higher priorities. Policy is retunable.
44
+ * @capability audio-voice-budget bound simultaneous sound playback with per-sound caps, priorities, and deterministic stealing
45
+ */
46
+ export declare function createVoiceAllocator(config?: VoiceAllocatorConfig): VoiceAllocator;
@@ -0,0 +1,76 @@
1
+ function limit(value) {
2
+ if (!Number.isFinite(value) || value < 0)
3
+ throw new RangeError("Voice limit must be finite and nonnegative");
4
+ return Math.floor(value);
5
+ }
6
+ function copy(state) {
7
+ return { nextId: state.nextId, voices: state.voices.map((voice) => ({ ...voice })) };
8
+ }
9
+ function weakest(voices) {
10
+ return [...voices].sort((a, b) => a.priority - b.priority || a.id - b.id);
11
+ }
12
+ /**
13
+ * Reserve bounded sound playback before fetching or constructing a graph. Equal priorities steal
14
+ * the oldest reservation; lower priorities cannot interrupt higher priorities. Policy is retunable.
15
+ * @capability audio-voice-budget bound simultaneous sound playback with per-sound caps, priorities, and deterministic stealing
16
+ */
17
+ export function createVoiceAllocator(config = {}) {
18
+ let maxTotal = limit(config.maxTotal ?? 64);
19
+ let overflow = config.overflow ?? "steal-lowest";
20
+ let memory = { nextId: 1, voices: [] };
21
+ const storage = config.storage ?? { read: () => memory, write: (next) => { memory = next; } };
22
+ function snapshot() { return copy(storage.read()); }
23
+ function restore(state) {
24
+ const ids = new Set();
25
+ if (!Number.isSafeInteger(state.nextId) || state.nextId < 1 || state.voices.length > maxTotal)
26
+ throw new RangeError("Invalid voice state");
27
+ for (const voice of state.voices) {
28
+ if (!Number.isSafeInteger(voice.id) || voice.id < 1 || voice.id >= state.nextId || ids.has(voice.id) || !Number.isFinite(voice.priority))
29
+ throw new RangeError("Invalid voice state");
30
+ ids.add(voice.id);
31
+ }
32
+ storage.write(copy(state));
33
+ }
34
+ restore(snapshot());
35
+ return {
36
+ snapshot, restore,
37
+ request(soundId, priority = 0, maxVoices = maxTotal) {
38
+ if (!Number.isFinite(priority))
39
+ throw new RangeError("Voice priority must be finite");
40
+ const soundLimit = limit(maxVoices);
41
+ if (maxTotal === 0 || soundLimit === 0)
42
+ return { ok: false };
43
+ const state = snapshot();
44
+ const same = weakest(state.voices.filter((voice) => voice.soundId === soundId));
45
+ const victims = same.slice(0, Math.max(0, same.length - soundLimit + 1));
46
+ const selected = new Set(victims.map((voice) => voice.id));
47
+ const remaining = weakest(state.voices.filter((voice) => !selected.has(voice.id)));
48
+ victims.push(...remaining.slice(0, Math.max(0, state.voices.length - victims.length - maxTotal + 1)));
49
+ if ((overflow === "reject" && victims.length > 0) || victims.some((voice) => voice.priority > priority))
50
+ return { ok: false };
51
+ const stolen = victims.map((voice) => voice.id);
52
+ const removed = new Set(stolen);
53
+ const voiceId = state.nextId++;
54
+ state.voices = state.voices.filter((voice) => !removed.has(voice.id));
55
+ state.voices.push({ id: voiceId, soundId, priority });
56
+ restore(state);
57
+ return { ok: true, voiceId, stolen };
58
+ },
59
+ release(voiceId) {
60
+ const state = snapshot();
61
+ state.voices = state.voices.filter((voice) => voice.id !== voiceId);
62
+ restore(state);
63
+ },
64
+ retune(next) {
65
+ const nextLimit = limit(next.maxTotal ?? maxTotal);
66
+ const state = snapshot();
67
+ const removed = weakest(state.voices).slice(0, Math.max(0, state.voices.length - nextLimit)).map((voice) => voice.id);
68
+ const ids = new Set(removed);
69
+ state.voices = state.voices.filter((voice) => !ids.has(voice.id));
70
+ maxTotal = nextLimit;
71
+ overflow = next.overflow ?? overflow;
72
+ restore(state);
73
+ return removed;
74
+ },
75
+ };
76
+ }
@@ -51,6 +51,21 @@ export interface AbilitySlotRetune {
51
51
  cooldownMs?: number;
52
52
  resourceCost?: number;
53
53
  }
54
+ /** Mutable kit state; slot/group definitions and any bound resource are saved by the caller. */
55
+ export interface AbilityKitState {
56
+ slots: {
57
+ id: string;
58
+ cooldownMs: number;
59
+ resourceCost: number;
60
+ charges: number;
61
+ rechargeRemainingMs: number;
62
+ flashRemainingMs: number;
63
+ }[];
64
+ groups: {
65
+ id: string;
66
+ remainingMs: number;
67
+ }[];
68
+ }
54
69
  export type AbilityCastResult = {
55
70
  ok: true;
56
71
  slot: AbilitySlotSnapshot;
@@ -62,8 +77,12 @@ export type AbilityCastResult = {
62
77
  export interface AbilityKit {
63
78
  slots(): readonly string[];
64
79
  config(slotId: string): AbilitySlotConfig | null;
80
+ /** Detached persistence state, including in-flight timers and slot retuning. */
81
+ state(): AbilityKitState;
65
82
  state(slotId: string, resourceAvailable?: number): AbilitySlotSnapshot | null;
66
83
  snapshot(resourceAvailable?: number): AbilitySlotSnapshot[];
84
+ /** Restore without spending resources; returns false without mutation for mismatched slot/group ids. */
85
+ restore(next: AbilityKitState): boolean;
67
86
  canCast(slotId: string, resourceAvailable?: number): AbilityCastResult;
68
87
  cast(slotId: string, resourceAvailable?: number): AbilityCastResult;
69
88
  tick(dtSeconds: number): void;
@@ -76,5 +95,6 @@ export interface AbilityKit {
76
95
  * A bar of cooldown-gated abilities the player fires by slot, tracking readiness and cooldown per ability.
77
96
  *
78
97
  * @capability ability-bar a bar of cooldown-gated abilities the player triggers by slot
98
+ * @capability global-cooldown share one cooldown across every ability slot (MMO global cooldown) with a cooldown group
79
99
  */
80
100
  export declare function createAbilityKit(configs: readonly AbilitySlotConfig[], options?: AbilityKitOptions): AbilityKit;
@@ -43,6 +43,7 @@ function snapshotOf(runtime, resourceAvailable, group) {
43
43
  * A bar of cooldown-gated abilities the player fires by slot, tracking readiness and cooldown per ability.
44
44
  *
45
45
  * @capability ability-bar a bar of cooldown-gated abilities the player triggers by slot
46
+ * @capability global-cooldown share one cooldown across every ability slot (MMO global cooldown) with a cooldown group
46
47
  */
47
48
  export function createAbilityKit(configs, options = {}) {
48
49
  const boundResource = options.resource;
@@ -107,6 +108,26 @@ export function createAbilityKit(configs, options = {}) {
107
108
  return fail(slotId, "no-resource", resourceAvailable);
108
109
  return { ok: true, slot: snapshotOf(runtime, resourceAvailable, groupBlockOf(runtime)) };
109
110
  }
111
+ function state(slotId, resourceAvailable) {
112
+ if (slotId !== undefined) {
113
+ const runtime = runtimes.get(slotId);
114
+ return runtime === undefined ? null : snapshotOf(runtime, currentResource(resourceAvailable), groupBlockOf(runtime));
115
+ }
116
+ return {
117
+ slots: order.map((id) => {
118
+ const runtime = runtimes.get(id);
119
+ return {
120
+ id,
121
+ cooldownMs: runtime.config.cooldownMs,
122
+ resourceCost: runtime.config.resourceCost,
123
+ charges: runtime.charges,
124
+ rechargeRemainingMs: runtime.rechargeRemainingMs,
125
+ flashRemainingMs: runtime.flashRemainingMs,
126
+ };
127
+ }),
128
+ groups: Array.from(groupRemaining, ([id, remainingMs]) => ({ id, remainingMs })),
129
+ };
130
+ }
110
131
  return {
111
132
  slots() {
112
133
  return order.slice();
@@ -114,10 +135,7 @@ export function createAbilityKit(configs, options = {}) {
114
135
  config(slotId) {
115
136
  return runtimes.get(slotId)?.config ?? null;
116
137
  },
117
- state(slotId, resourceAvailable) {
118
- const runtime = runtimes.get(slotId);
119
- return runtime === undefined ? null : snapshotOf(runtime, currentResource(resourceAvailable), groupBlockOf(runtime));
120
- },
138
+ state,
121
139
  snapshot(resourceAvailable) {
122
140
  const available = currentResource(resourceAvailable);
123
141
  return order.map((slotId) => {
@@ -125,6 +143,25 @@ export function createAbilityKit(configs, options = {}) {
125
143
  return snapshotOf(runtime, available, groupBlockOf(runtime));
126
144
  });
127
145
  },
146
+ restore(next) {
147
+ if (next.slots.length !== runtimes.size || next.groups.length !== groupRemaining.size ||
148
+ new Set(next.slots.map((slot) => slot.id)).size !== runtimes.size ||
149
+ new Set(next.groups.map((group) => group.id)).size !== groupRemaining.size ||
150
+ next.slots.some((slot) => !runtimes.has(slot.id)) ||
151
+ next.groups.some((group) => !groupRemaining.has(group.id)))
152
+ return false;
153
+ for (const slot of next.slots) {
154
+ const runtime = runtimes.get(slot.id);
155
+ runtime.config.cooldownMs = Math.max(0, slot.cooldownMs);
156
+ runtime.config.resourceCost = Math.max(0, slot.resourceCost);
157
+ runtime.charges = Math.max(0, Math.min(runtime.config.chargesMax, slot.charges));
158
+ runtime.rechargeRemainingMs = Math.max(0, slot.rechargeRemainingMs);
159
+ runtime.flashRemainingMs = Math.max(0, slot.flashRemainingMs);
160
+ }
161
+ for (const group of next.groups)
162
+ groupRemaining.set(group.id, Math.max(0, group.remainingMs));
163
+ return true;
164
+ },
128
165
  canCast(slotId, resourceAvailable) {
129
166
  return evaluate(slotId, currentResource(resourceAvailable));
130
167
  },
@@ -6,8 +6,9 @@ import type { EffectVia } from "./effects.js";
6
6
  export type DeathReasonKind = DeathReason["kind"];
7
7
  export interface OnDeathDropRule {
8
8
  table: string;
9
+ /** `"any"` explicitly permits world item drops without a player killer. */
9
10
  when?: {
10
- reason: DeathReasonKind;
11
+ reason: DeathReasonKind | "any";
11
12
  };
12
13
  }
13
14
  export interface OnDeathCommandRule {
@@ -18,6 +19,13 @@ export interface OnDeathCommandRule {
18
19
  };
19
20
  }
20
21
  export type DropMode = "grant" | "world";
22
+ /**
23
+ * Catalog `onDeath` for an entity: loot tables to roll, `dropMode: "world"` to scatter drops on the ground
24
+ * (rendered with loot beams) instead of granting them into the killer's bag, and commands to run. Fires only
25
+ * when the entity dies through `ctx.scene.entity.effect`, not a raw `stats.delta`.
26
+ *
27
+ * @capability on-death-drops make a killed enemy drop loot on the ground or into the killer's bag via catalog onDeath
28
+ */
21
29
  export interface OnDeathSpec {
22
30
  drops?: string | OnDeathDropRule[];
23
31
  command?: string | OnDeathCommandRule;
@@ -40,13 +48,13 @@ export interface DeathIdentity {
40
48
  position: [number, number, number];
41
49
  }
42
50
  export interface DeathSystemDeps {
43
- resolveOnDeath(instanceId: string): OnDeathSpec | null | undefined;
51
+ resolveOnDeath(instanceId: string, reason?: DeathReason): OnDeathSpec | null | undefined;
44
52
  resolveIdentity(instanceId: string): DeathIdentity | null;
45
53
  loot: {
46
54
  roll(tableId: string): Drop[];
47
55
  };
48
56
  events: GameEvents;
49
- runCommand?(name: string, args: unknown): void;
57
+ runCommand?(name: string, args: unknown, reason?: DeathReason): void;
50
58
  despawn(instanceId: string): void;
51
59
  }
52
60
  export type DeathResolution = {