@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,8 +1,15 @@
1
1
  import { createDecisionGraphRuntime } from "../ai/decisionGraph.js";
2
- import { advanceInterestGate, createInterestGateState } from "../ai/interestScheduler.js";
2
+ import { advanceInterestGate, createInterestGateState, interestPhase } from "../ai/interestScheduler.js";
3
+ import { advancePursuit, armPursuit, createPursuitState } from "../ai/pursuit.js";
4
+ import { acquireTarget } from "../ai/targetAcquisition.js";
5
+ import { createThreatTable } from "../ai/threat.js";
3
6
  import { advancePathFollow, createPathFollow, pathFollowSeek, } from "../nav/pathFollow.js";
4
7
  import { resolveWalkerStep } from "../movement/solidObstacles.js";
5
8
  import { perContext } from "../runtime/perContext.js";
9
+ import { notifyAfter } from "../store/changeSignal.js";
10
+ import { visibleEntityIds } from "../runtime/worldProjection.js";
11
+ import { behaviorDataEqual } from "./behaviors.js";
12
+ import { distanceBetween } from "./spatial.js";
6
13
  const DEFAULT_WANDER_SPEED = 1.5;
7
14
  const WANDER_ARRIVAL = 0.6;
8
15
  /** Below this share of its intended step a wanderer counts as blocked and re-rolls its target. */
@@ -11,14 +18,19 @@ const behaviorActions = new Map();
11
18
  /** Register the named actions used by decision graph behavior descriptors.
12
19
  * @capability behavior-actions register named callbacks for decision graph behavior descriptors
13
20
  */
14
- export function registerBehaviorActions(id, actions) {
15
- behaviorActions.set(id, actions);
21
+ export function registerBehaviorActions(id, actions, options = {}) {
22
+ const entry = { actions, options };
23
+ behaviorActions.set(id, entry);
16
24
  return () => {
17
- if (behaviorActions.get(id) === actions)
25
+ if (behaviorActions.get(id) === entry)
18
26
  behaviorActions.delete(id);
19
27
  };
20
28
  }
21
- const DECISION_GRAPH_CADENCE = { wakeRadius: Number.POSITIVE_INFINITY, activeInterval: 0 };
29
+ // Bind unsaved policy without eagerly constructing the lazy AI runtime before boot-time restore.
30
+ const pursuitPolicies = new WeakMap();
31
+ function thinkCadence(behavior) {
32
+ return { wakeRadius: Number.POSITIVE_INFINITY, activeInterval: Math.max(0, behavior.thinkInterval ?? 0) };
33
+ }
22
34
  function patrolOf(entity) {
23
35
  return entity.behaviors.find((b) => b.kind === "patrol") ?? null;
24
36
  }
@@ -30,12 +42,20 @@ function decisionGraphOf(entity) {
30
42
  }
31
43
  const runtimeOf = perContext((ctx) => {
32
44
  const nav = new Map();
33
- const refresh = () => {
34
- const live = new Set();
35
- for (const entity of ctx.scene.entity.list()) {
36
- live.add(entity.id);
37
- if (nav.has(entity.id))
38
- continue;
45
+ const descriptors = new Map();
46
+ const refreshId = (id) => {
47
+ const entity = ctx.scene.entity.get(id);
48
+ const descriptor = entity === null ? undefined : patrolOf(entity) ?? wanderOf(entity) ?? decisionGraphOf(entity)
49
+ ?? entity.behaviors.find(behavior => behavior.kind === "pursue");
50
+ if (entity !== null && descriptor !== undefined && nav.has(id) && behaviorDataEqual(descriptors.get(id), descriptor))
51
+ return;
52
+ const previous = nav.get(id);
53
+ nav.delete(id);
54
+ descriptors.delete(id);
55
+ try {
56
+ if (entity === null || descriptor === undefined)
57
+ return;
58
+ descriptors.set(id, descriptor);
39
59
  const patrol = patrolOf(entity);
40
60
  if (patrol !== null) {
41
61
  const config = { waypoints: patrol.waypoints, speed: patrol.speed, loop: patrol.loop };
@@ -48,7 +68,7 @@ const runtimeOf = perContext((ctx) => {
48
68
  reason: null,
49
69
  pausedElapsed: 0,
50
70
  });
51
- continue;
71
+ return;
52
72
  }
53
73
  const wander = wanderOf(entity);
54
74
  if (wander !== null) {
@@ -62,32 +82,165 @@ const runtimeOf = perContext((ctx) => {
62
82
  reason: null,
63
83
  pausedElapsed: 0,
64
84
  });
65
- continue;
85
+ return;
66
86
  }
67
87
  const decisionGraph = decisionGraphOf(entity);
68
88
  if (decisionGraph !== null) {
69
- const actions = behaviorActions.get(decisionGraph.actions);
70
- if (actions === undefined)
71
- continue;
89
+ const registered = behaviorActions.get(decisionGraph.actions);
90
+ if (registered === undefined)
91
+ return;
92
+ const cadence = thinkCadence(decisionGraph);
72
93
  nav.set(entity.id, {
73
94
  kind: "decisionGraph",
74
95
  behavior: decisionGraph,
75
- runtime: createDecisionGraphRuntime(decisionGraph.graph, actions),
76
- gate: createInterestGateState(DECISION_GRAPH_CADENCE, ctx.rng()),
96
+ runtime: createDecisionGraphRuntime(decisionGraph.graph, registered.actions, {
97
+ rng: () => ctx.rng(),
98
+ ...(registered.options.onAbort === undefined ? {} : { onAbort: registered.options.onAbort }),
99
+ }),
100
+ gate: createInterestGateState(cadence, ctx.rng()),
101
+ cadence,
102
+ blackboard: { ...(decisionGraph.blackboard ?? {}) },
103
+ pending: 0,
77
104
  status: "active",
78
105
  reason: null,
79
106
  pausedElapsed: 0,
80
107
  });
81
108
  }
109
+ const pursuing = entity.behaviors.find((behavior) => behavior.kind === "pursue");
110
+ if (pursuing?.kind === "pursue" && !nav.has(entity.id)) {
111
+ const cadence = { wakeRadius: Infinity, activeInterval: pursuing.thinkInterval ?? .1 };
112
+ const gate = createInterestGateState(cadence, interestPhase(entity.id));
113
+ gate.state = "active";
114
+ nav.set(entity.id, { kind: "pursue", behavior: pursuing,
115
+ home: [...(ctx.scene.entity.spawnPoseOf(entity.id)?.position ?? entity.position)],
116
+ targetId: null, returning: false, pursuit: createPursuitState(),
117
+ gate, cadence, pending: 0,
118
+ status: "active", reason: null, pausedElapsed: 0,
119
+ ...(pursuing.threat === undefined ? {} : { threat: createThreatTable(pursuing.threat) }) });
120
+ }
82
121
  }
83
- for (const id of nav.keys())
122
+ finally {
123
+ if (previous?.kind === "decisionGraph")
124
+ previous.runtime.abort({ ctx, entityId: id, dt: 0 }, previous.blackboard);
125
+ }
126
+ };
127
+ const refresh = () => {
128
+ const live = new Set();
129
+ for (const entity of ctx.scene.entity.list()) {
130
+ live.add(entity.id);
131
+ refreshId(entity.id);
132
+ }
133
+ for (const id of descriptors.keys())
84
134
  if (!live.has(id))
85
- nav.delete(id);
135
+ refreshId(id);
86
136
  };
87
- ctx.scene.entity.subscribeMembership(refresh);
137
+ ctx.scene.entity.subscribeBehaviors(refreshId);
88
138
  refresh();
89
139
  return { nav };
90
140
  });
141
+ function resetPursue(ctx, id, entry) {
142
+ entry.targetId = null;
143
+ entry.returning = false;
144
+ entry.pursuit.attackCooldown = 0;
145
+ entry.pending = 0;
146
+ entry.gate = createInterestGateState(entry.cadence, interestPhase(id));
147
+ entry.gate.state = "active";
148
+ entry.threat?.clear();
149
+ ctx.scene.entity.setTarget(id, null);
150
+ }
151
+ function object(value) {
152
+ return value !== null && typeof value === "object" && !Array.isArray(value);
153
+ }
154
+ function nonnegative(value) {
155
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
156
+ }
157
+ function validPursueSnapshot(value) {
158
+ if (!object(value) || value.kind !== "pursue" || !Array.isArray(value.home) || value.home.length !== 3
159
+ || !value.home.every(n => typeof n === "number" && Number.isFinite(n))
160
+ || !(value.targetId === null || typeof value.targetId === "string")
161
+ || !(value.explicitTargetId === null || typeof value.explicitTargetId === "string") || typeof value.returning !== "boolean"
162
+ || !object(value.pursuit) || !nonnegative(value.pursuit.attackCooldown) || !nonnegative(value.pending)
163
+ || !object(value.gate) || !(value.gate.state === "active" || value.gate.state === "dormant")
164
+ || !nonnegative(value.gate.clock) || !nonnegative(value.gate.awakeHold)
165
+ || !nonnegative(value.gate.phase) || value.gate.phase >= 1)
166
+ return false;
167
+ if (value.threat !== undefined) {
168
+ if (!object(value.threat) || !Array.isArray(value.threat.entries) || !nonnegative(value.threat.forcedRemaining)
169
+ || !(value.threat.forcedSource === null || typeof value.threat.forcedSource === "string"))
170
+ return false;
171
+ const seen = new Set();
172
+ for (const entry of value.threat.entries) {
173
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || seen.has(entry[0])
174
+ || typeof entry[1] !== "number" || !Number.isFinite(entry[1]))
175
+ return false;
176
+ seen.add(entry[0]);
177
+ }
178
+ }
179
+ return true;
180
+ }
181
+ function restorePursue(entry, snapshot) {
182
+ entry.home = [...snapshot.home];
183
+ entry.targetId = snapshot.targetId;
184
+ entry.returning = snapshot.returning;
185
+ entry.pursuit = { ...snapshot.pursuit };
186
+ entry.gate = { ...snapshot.gate };
187
+ entry.pending = snapshot.pending;
188
+ if (entry.threat !== undefined && snapshot.threat !== undefined)
189
+ entry.threat.restore(snapshot.threat);
190
+ else
191
+ entry.threat?.clear();
192
+ }
193
+ function stepPursue(ctx, id, entry, dt) {
194
+ const entity = ctx.scene.entity.get(id);
195
+ if (entity === null)
196
+ return;
197
+ const { behavior, home } = entry;
198
+ const homeDistance = distanceBetween(entity.position, home);
199
+ const policy = pursuitPolicies.get(ctx);
200
+ const eligible = (candidate) => candidate !== id && ctx.scene.entity.get(candidate) !== null
201
+ && ctx.scene.entity.canReceive(candidate, behavior.attack.effect, behavior.attack.amount) === null
202
+ && (policy === undefined || policy(ctx, id, candidate) === true);
203
+ entry.threat?.decay(dt);
204
+ if (!entry.returning) {
205
+ const explicit = ctx.scene.entity.getTarget(id);
206
+ const forced = entry.threat?.forcedTarget() ?? null;
207
+ if (explicit !== null && eligible(explicit))
208
+ entry.targetId = explicit;
209
+ else if (forced !== null && eligible(forced))
210
+ entry.targetId = forced;
211
+ else {
212
+ const nearby = ctx.scene.entity.inRadius(id, behavior.aggroRadius, candidate => ctx.scene.entity.get(candidate)?.role === "player");
213
+ if (entry.targetId !== null && !nearby.includes(entry.targetId))
214
+ nearby.push(entry.targetId);
215
+ const held = entry.targetId;
216
+ entry.targetId = acquireTarget({ candidates: () => nearby, distance: ctx.scene.entity.distance,
217
+ range: (_self, candidate) => candidate === held ? Infinity : behavior.aggroRadius,
218
+ eligible: (_self, candidate) => eligible(candidate), score: (_self, candidate) => entry.threat?.threatOf(candidate) ?? 0,
219
+ tieBreak: (a, b) => (ctx.scene.entity.distance(id, a) ?? Infinity) - (ctx.scene.entity.distance(id, b) ?? Infinity)
220
+ || (a < b ? -1 : a > b ? 1 : 0),
221
+ }, id, held).targetId;
222
+ }
223
+ }
224
+ const targetDistance = entry.targetId === null ? null : ctx.scene.entity.distance(id, entry.targetId);
225
+ const action = advancePursuit(entry.pursuit, dt, targetDistance, behavior.reach, behavior.cooldownMode, homeDistance, behavior.leashRange);
226
+ if (action === "leash" || (targetDistance === null && homeDistance > .1))
227
+ entry.returning = true;
228
+ if (entry.returning) {
229
+ entry.targetId = null;
230
+ ctx.scene.entity.setTarget(id, null);
231
+ ctx.scene.entity.moveTowardCommit(id, home, { speed: behavior.speed, stopDistance: 0, dt, face: true });
232
+ const position = ctx.scene.entity.get(id)?.position;
233
+ if (position !== undefined && distanceBetween(position, home) <= .1)
234
+ resetPursue(ctx, id, entry);
235
+ }
236
+ else if (action === "pursue" && entry.targetId !== null) {
237
+ ctx.scene.entity.moveTowardCommit(id, entry.targetId, { speed: behavior.speed, stopDistance: behavior.reach, dt, face: true });
238
+ }
239
+ else if (action === "attack" && entry.targetId !== null) {
240
+ ctx.scene.entity.effect({ from: id, to: entry.targetId, effect: behavior.attack.effect, via: { amount: behavior.attack.amount } });
241
+ armPursuit(entry.pursuit, behavior.attack.intervalSec);
242
+ }
243
+ }
91
244
  function stepWander(ctx, id, nav, dt) {
92
245
  const entity = ctx.scene.entity.get(id);
93
246
  if (entity === null)
@@ -143,13 +296,13 @@ function posePatrol(ctx, id, nav, dt) {
143
296
  });
144
297
  }
145
298
  /**
146
- * Advance every spawned entity carrying a `patrol` or `wander` {@link BehaviorDescriptor} one tick — the
299
+ * Advance every spawned entity carrying a patrol, wander, pursuit or decision graph behavior one tick — the
147
300
  * engine reads the descriptor, keeps the per-entity nav state itself, and poses the entity, so ambient
148
301
  * traffic and idle NPC routes are register-once (attach the behavior at spawn) instead of a per-game
149
302
  * per-frame `advancePathFollow` + `setPose` loop. Instances that are paused or disabled through
150
303
  * {@link behaviorControl} retain their state and are skipped. The shell/host call this each frame; a game never does.
151
304
  *
152
- * @capability behavior-tick auto-advance patrol/wander behaviors on spawned entities, no per-game route loop
305
+ * @capability behavior-tick auto-advance spawned patrol, wander, pursuit and decision graph behaviors with lifecycle control
153
306
  */
154
307
  export function advanceBehaviors(ctx, dt) {
155
308
  if (dt <= 0)
@@ -157,10 +310,14 @@ export function advanceBehaviors(ctx, dt) {
157
310
  const { nav } = runtimeOf(ctx);
158
311
  if (nav.size === 0)
159
312
  return;
313
+ let pursuitChanged = false;
160
314
  for (const [id, entry] of nav) {
161
315
  if (entry.status !== "active") {
162
- if (entry.status === "paused")
316
+ if (entry.status === "paused") {
163
317
  entry.pausedElapsed += dt;
318
+ if (entry.kind === "pursue")
319
+ pursuitChanged = true;
320
+ }
164
321
  continue;
165
322
  }
166
323
  if (entry.kind === "patrol") {
@@ -170,19 +327,30 @@ export function advanceBehaviors(ctx, dt) {
170
327
  else if (entry.kind === "wander") {
171
328
  stepWander(ctx, id, entry, dt);
172
329
  }
330
+ else if (entry.kind === "pursue") {
331
+ entry.pending += dt;
332
+ pursuitChanged = true;
333
+ if (!advanceInterestGate(entry.gate, entry.cadence, dt, { proximity: 0 }).active)
334
+ continue;
335
+ const elapsed = entry.pending;
336
+ entry.pending = 0;
337
+ stepPursue(ctx, id, entry, elapsed);
338
+ }
173
339
  else {
174
340
  const entity = ctx.scene.entity.get(id);
175
341
  if (entity === null)
176
342
  continue;
177
- const step = advanceInterestGate(entry.gate, DECISION_GRAPH_CADENCE, dt, { proximity: 0 });
343
+ entry.pending += dt;
344
+ const step = advanceInterestGate(entry.gate, entry.cadence, dt, { proximity: 0 });
178
345
  if (!step.active)
179
346
  continue;
180
- const blackboard = {};
181
- const status = entry.runtime.tick({ ctx, entityId: id, dt }, blackboard, dt);
182
- if (status === "done" || status === "failed")
183
- entry.runtime.restore({ runningPath: null });
347
+ const elapsed = entry.pending;
348
+ entry.pending = 0;
349
+ entry.runtime.tick({ ctx, entityId: id, dt: elapsed }, entry.blackboard, elapsed);
184
350
  }
185
351
  }
352
+ if (pursuitChanged)
353
+ ctx.touch();
186
354
  }
187
355
  /**
188
356
  * Obtain the per-context {@link BehaviorControl} surface for suspending, resuming, seeking, serializing,
@@ -199,6 +367,8 @@ export function behaviorControl(ctx) {
199
367
  return false;
200
368
  entry.status = status;
201
369
  entry.reason = reason;
370
+ if (entry.kind === "pursue")
371
+ ctx.touch();
202
372
  return true;
203
373
  };
204
374
  return {
@@ -211,6 +381,8 @@ export function behaviorControl(ctx) {
211
381
  entry.status = "paused";
212
382
  entry.reason = reason ?? null;
213
383
  entry.pausedElapsed = 0;
384
+ if (entry.kind === "pursue")
385
+ ctx.touch();
214
386
  return true;
215
387
  },
216
388
  resume: (id, policy = "freeze") => {
@@ -225,6 +397,8 @@ export function behaviorControl(ctx) {
225
397
  entry.status = "active";
226
398
  entry.reason = null;
227
399
  entry.pausedElapsed = 0;
400
+ if (entry.kind === "pursue")
401
+ ctx.touch();
228
402
  return true;
229
403
  },
230
404
  disable: (id, reason) => setStatus(id, "disabled", reason ?? null),
@@ -235,6 +409,8 @@ export function behaviorControl(ctx) {
235
409
  entry.status = "active";
236
410
  entry.reason = null;
237
411
  entry.pausedElapsed = 0;
412
+ if (entry.kind === "pursue")
413
+ ctx.touch();
238
414
  return true;
239
415
  },
240
416
  seek: (id, progress) => {
@@ -252,7 +428,12 @@ export function behaviorControl(ctx) {
252
428
  return { kind: "patrol", state: { ...entry.state } };
253
429
  if (entry.kind === "wander")
254
430
  return { kind: "wander", origin: [...entry.origin], target: entry.target === null ? null : [...entry.target] };
255
- return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate } };
431
+ if (entry.kind === "pursue")
432
+ return { kind: "pursue", home: [...entry.home], targetId: entry.targetId,
433
+ explicitTargetId: ctx.scene.entity.getTarget(id),
434
+ returning: entry.returning, pursuit: { ...entry.pursuit }, gate: { ...entry.gate }, pending: entry.pending,
435
+ ...(entry.threat === undefined ? {} : { threat: entry.threat.snapshot() }) };
436
+ return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate }, blackboard: { ...entry.blackboard }, pending: entry.pending };
256
437
  },
257
438
  restore: (id, snapshot) => {
258
439
  const entry = nav.get(id);
@@ -265,6 +446,8 @@ export function behaviorControl(ctx) {
265
446
  if (entry.kind === "decisionGraph" && snapshot.kind === "decisionGraph") {
266
447
  entry.runtime.restore(snapshot.graph);
267
448
  entry.gate = { ...snapshot.gate };
449
+ entry.blackboard = { ...(snapshot.blackboard ?? entry.behavior.blackboard ?? {}) };
450
+ entry.pending = snapshot.pending ?? 0;
268
451
  return true;
269
452
  }
270
453
  if (entry.kind === "wander" && snapshot.kind === "wander") {
@@ -272,6 +455,14 @@ export function behaviorControl(ctx) {
272
455
  entry.target = snapshot.target === null ? null : [...snapshot.target];
273
456
  return true;
274
457
  }
458
+ if (entry.kind === "pursue" && snapshot.kind === "pursue") {
459
+ if (!validPursueSnapshot(snapshot))
460
+ return false;
461
+ restorePursue(entry, snapshot);
462
+ ctx.scene.entity.setTarget(id, snapshot.explicitTargetId);
463
+ ctx.touch();
464
+ return true;
465
+ }
275
466
  return false;
276
467
  },
277
468
  inspect: (id) => {
@@ -286,5 +477,77 @@ export function behaviorControl(ctx) {
286
477
  out.push({ id, kind: entry.kind, status: entry.status, reason: entry.reason });
287
478
  return out;
288
479
  },
480
+ blackboard: (id) => {
481
+ const entry = nav.get(id);
482
+ return entry?.kind === "decisionGraph" ? entry.blackboard : null;
483
+ },
484
+ threat: (id) => {
485
+ const entry = nav.get(id);
486
+ if (entry?.kind !== "pursue" || entry.threat === undefined)
487
+ return null;
488
+ return entry.threatControl ??= notifyAfter(entry.threat, ["add", "set", "taunt", "remove", "clear", "restore", "decay"], ctx.touch);
489
+ },
490
+ reset: (id) => {
491
+ const entry = nav.get(id);
492
+ if (entry?.kind !== "pursue")
493
+ return false;
494
+ resetPursue(ctx, id, entry);
495
+ ctx.touch();
496
+ return true;
497
+ },
289
498
  };
290
499
  }
500
+ /** @internal Register persistence before boot-time restore without constructing an AI runtime. */
501
+ export function installPursuitPersistence(ctx, aoiRadius, eligible) {
502
+ if (eligible !== undefined)
503
+ pursuitPolicies.set(ctx, eligible);
504
+ ctx.game.registerReplicate?.({
505
+ key: "pursuitBehaviors",
506
+ snapshot() {
507
+ const control = behaviorControl(ctx);
508
+ const instances = [];
509
+ for (const [id, entry] of runtimeOf(ctx).nav) {
510
+ if (entry.kind !== "pursue")
511
+ continue;
512
+ instances.push({ id, snapshot: control.serialize(id),
513
+ status: entry.status, reason: entry.reason, pausedElapsed: entry.pausedElapsed });
514
+ }
515
+ return { version: 1, instances };
516
+ },
517
+ decode(raw) {
518
+ if (!object(raw) || raw.version !== 1 || !Array.isArray(raw.instances))
519
+ return null;
520
+ const seen = new Set();
521
+ for (const value of raw.instances) {
522
+ if (!object(value) || typeof value.id !== "string" || seen.has(value.id)
523
+ || !validPursueSnapshot(value.snapshot) || !(value.status === "active" || value.status === "paused" || value.status === "disabled")
524
+ || !(value.reason === null || typeof value.reason === "string") || !nonnegative(value.pausedElapsed)
525
+ || !ctx.scene.entity.get(value.id)?.behaviors.some(behavior => behavior.kind === "pursue"))
526
+ return null;
527
+ seen.add(value.id);
528
+ }
529
+ return structuredClone(raw);
530
+ },
531
+ hydrate(data) {
532
+ const runtime = runtimeOf(ctx);
533
+ for (const record of data.instances) {
534
+ const entry = runtime.nav.get(record.id);
535
+ if (entry?.kind !== "pursue")
536
+ continue;
537
+ restorePursue(entry, record.snapshot);
538
+ entry.status = record.status;
539
+ entry.reason = record.reason;
540
+ entry.pausedElapsed = record.pausedElapsed;
541
+ }
542
+ for (const record of data.instances)
543
+ ctx.scene.entity.setTarget(record.id, record.snapshot.explicitTargetId);
544
+ ctx.touch();
545
+ },
546
+ ...(aoiRadius === undefined ? {} : {
547
+ project(data, viewer, world) {
548
+ const visible = visibleEntityIds((world.entities ?? []), viewer, aoiRadius);
549
+ return { ...data, instances: data.instances.filter(record => visible.has(record.id)) };
550
+ },
551
+ }),
552
+ });
553
+ }
@@ -1,6 +1,29 @@
1
1
  import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
2
2
  import type { PathProgress, Waypoint } from "../nav/pathFollow.js";
3
- import type { DecisionGraph } from "../ai/decisionGraph.js";
3
+ import type { Blackboard, DecisionGraph } from "../ai/decisionGraph.js";
4
+ import type { CooldownMode } from "../ai/pursuit.js";
5
+ import type { ThreatTableConfig } from "../ai/threat.js";
6
+ /** Scheduled chase/attack behavior; the default declares hostility toward player-role entities.
7
+ * GameDefinition.pursuit.eligible can further restrict every target source using the live world.
8
+ * Explicit targets and optional threat override nearest-player acquisition, not eligibility.
9
+ */
10
+ export interface PursueBehavior {
11
+ kind: "pursue";
12
+ aggroRadius: number;
13
+ reach: number;
14
+ leashRange: number;
15
+ speed: number;
16
+ attack: {
17
+ effect: string;
18
+ amount: number;
19
+ intervalSec: number;
20
+ };
21
+ /** Seconds between acquisition/movement/attack ticks; default .1. Zero advances every frame. */
22
+ thinkInterval?: number;
23
+ cooldownMode?: CooldownMode;
24
+ /** Opt into a per-instance table available through `behaviorControl(ctx).threat(id)`. */
25
+ threat?: ThreatTableConfig;
26
+ }
4
27
  export interface WanderBehavior {
5
28
  kind: "wander";
6
29
  radius: number;
@@ -33,8 +56,18 @@ export interface DecisionGraphBehavior {
33
56
  kind: "decisionGraph";
34
57
  graph: DecisionGraph;
35
58
  actions: string;
59
+ /** Initial facts for this entity's persistent blackboard; systems write more through `behaviorControl(ctx).blackboard(id)`. */
60
+ blackboard?: Blackboard;
61
+ /** Seconds between graph ticks (`0` = every frame). Siblings are staggered; actions receive the elapsed time. */
62
+ thinkInterval?: number;
36
63
  }
37
- export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior | DecisionGraphBehavior;
64
+ export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior | PursueBehavior | DecisionGraphBehavior;
65
+ /** @internal Compare declarative behavior data at replacement boundaries, independent of object key order. */
66
+ export declare function behaviorDataEqual(a: unknown, b: unknown): boolean;
67
+ /** Attach aggro, collision-aware chase, cooldown attack and latched return-to-spawn as data.
68
+ * @capability pursue-behavior scheduled pursuit with bounded target acquisition and saved cooldown/leash state
69
+ */
70
+ export declare function pursue(options: Omit<PursueBehavior, "kind">): PursueBehavior;
38
71
  export declare function wander({ radius }: {
39
72
  radius: number;
40
73
  }): WanderBehavior;
@@ -1,4 +1,33 @@
1
1
  import { command, keybind, proximityPrompt, } from "../interaction/proximityPrompt.js";
2
+ /** @internal Compare declarative behavior data at replacement boundaries, independent of object key order. */
3
+ export function behaviorDataEqual(a, b) {
4
+ if (Object.is(a, b))
5
+ return true;
6
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
7
+ return false;
8
+ if (Array.isArray(a) || Array.isArray(b))
9
+ return Array.isArray(a) && Array.isArray(b)
10
+ && a.length === b.length && a.every((value, index) => behaviorDataEqual(value, b[index]));
11
+ const left = a, right = b;
12
+ const keys = Object.keys(left).filter(key => left[key] !== undefined);
13
+ return keys.length === Object.keys(right).filter(key => right[key] !== undefined).length
14
+ && keys.every(key => Object.hasOwn(right, key) && behaviorDataEqual(left[key], right[key]));
15
+ }
16
+ /** Attach aggro, collision-aware chase, cooldown attack and latched return-to-spawn as data.
17
+ * @capability pursue-behavior scheduled pursuit with bounded target acquisition and saved cooldown/leash state
18
+ */
19
+ export function pursue(options) {
20
+ for (const [name, value] of Object.entries({ aggroRadius: options.aggroRadius, reach: options.reach,
21
+ leashRange: options.leashRange, speed: options.speed, intervalSec: options.attack.intervalSec,
22
+ thinkInterval: options.thinkInterval ?? .1 })) {
23
+ if (!Number.isFinite(value) || value < 0)
24
+ throw new Error(`pursue: ${name} must be finite and nonnegative`);
25
+ }
26
+ if (!Number.isFinite(options.attack.amount))
27
+ throw new Error("pursue: attack amount must be finite");
28
+ return { ...options, kind: "pursue", attack: { ...options.attack },
29
+ ...(options.threat === undefined ? {} : { threat: { ...options.threat } }) };
30
+ }
2
31
  export function wander({ radius }) {
3
32
  return { kind: "wander", radius };
4
33
  }
@@ -1,4 +1,4 @@
1
- import type { BehaviorDescriptor } from "./behaviors.js";
1
+ import { type BehaviorDescriptor } from "./behaviors.js";
2
2
  export type EntityRole = "player" | "npc" | "prop";
3
3
  export interface EntityMovement {
4
4
  walkSpeed?: number;
@@ -17,7 +17,7 @@ export interface SceneEntity<TMeta = unknown> {
17
17
  rotationY: number;
18
18
  rotationX: number;
19
19
  rotationZ: number;
20
- /** World units per second, derived from consecutive setPose calls that carry `dt`. Zero until the entity moves under a dt. */
20
+ /** World units per second, derived by `setPose` with `dt` or explicitly replaced by `setVelocity`. Zero until set. */
21
21
  velocity: EntityPosition;
22
22
  role: EntityRole;
23
23
  movement: EntityMovement;
@@ -97,11 +97,14 @@ export interface EntityBlackboard {
97
97
  /** Milliseconds until the armed cooldown elapses; 0 when ready. */
98
98
  remaining(id: string, key: string, nowMs: number): number;
99
99
  }
100
+ /** Entity state, pose writes, keyed behavior notifications and snapshot/hydration under stable instance IDs. */
100
101
  export interface EntityStore<TMeta = unknown> {
101
102
  spawn(name: string, options?: SpawnOptions<TMeta>): string;
102
103
  despawn(id: string): boolean;
103
104
  update(id: string, patch: EntityUpdatePatch<TMeta>): boolean;
104
105
  setPose(id: string, pose: EntityPose): boolean;
106
+ /** Replace velocity without moving the entity. Copies a finite three-number tuple; invalid input or a missing id returns false unchanged. */
107
+ setVelocity(id: string, velocity: EntityPosition): boolean;
105
108
  /** Register a constraint applied inside every `setPose` for this entity — the self-driven sibling of the shell's `beforeCommit` (#282.9): nav clamps, corridor walls, arena bounds without wrapping every call site. `null` clears; despawn clears automatically. */
106
109
  setPoseConstraint(id: string, constraint: PoseConstraint | null): void;
107
110
  get(id: string): SceneEntity<TMeta> | null;
@@ -112,6 +115,8 @@ export interface EntityStore<TMeta = unknown> {
112
115
  subscribe(listener: () => void): () => void;
113
116
  /** Notified only when the entity set changes (spawn/despawn/hydrate), never on a pose or field update — the render tree re-reconciles on membership, individual markers read live poses imperatively. */
114
117
  subscribeMembership(listener: () => void): () => void;
118
+ /** Changed behavior data or removal, keyed by id and notified before ordinary subscribers. Equal-data hydration and pose writes do not fire. */
119
+ subscribeBehaviors(listener: (id: string) => void): () => void;
115
120
  snapshot(): readonly SceneEntity<TMeta>[];
116
121
  /**
117
122
  * Apply a `snapshot()` (e.g. from an authoritative host) to this store — the counterpart of `snapshot()`
@@ -123,4 +128,10 @@ export interface EntityStore<TMeta = unknown> {
123
128
  resetToSpawn(id: string): boolean;
124
129
  blackboard: EntityBlackboard;
125
130
  }
126
- export declare function createEntityStore<TMeta = unknown>(): EntityStore<TMeta>;
131
+ /** Commit hook for a derived index; runs after the write and before existing subscribers.
132
+ * `undefined` denotes a whole-store replacement. Value overwrites keep membership false.
133
+ */
134
+ export interface EntityStoreOptions {
135
+ onChange?: (instanceId: string | undefined, membershipChanged: boolean) => void;
136
+ }
137
+ export declare function createEntityStore<TMeta = unknown>(options?: EntityStoreOptions): EntityStore<TMeta>;