@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
@@ -2,28 +2,45 @@ import { createDeathSystem, deathReasonFromEffect } from "../../combat/death.js"
2
2
  import { createEffectSystem, } from "../../combat/effects.js";
3
3
  import { createProjectileSystem } from "../../combat/projectiles.js";
4
4
  import { createVfxInstanceStore } from "../../game/vfxInstance.js";
5
+ import { colliderBounds, colliderWorldCenter, defaultEntityColliders, resolveColliders } from "../../scene/colliders.js";
6
+ import { raycastCollisionMesh } from "../../scene/collisionMesh.js";
7
+ import { sweepMovingBounds, sweepMovingSphere } from "../../physics/ballisticSweep.js";
8
+ import { createSpatialIndex } from "../../visibility/spatialIndex.js";
5
9
  import { createEntityStatsApi } from "../../scene/entityStats.js";
6
10
  import { notifyAfter } from "../../store/changeSignal.js";
7
11
  import { createCombatFx } from "./combatFx.js";
8
- import { applyLethalLoot, isLocalPlayerKill } from "./deathLoot.js";
12
+ import { allowsUnownedWorldDrops, applyLethalLoot } from "./deathLoot.js";
9
13
  /** @internal */
10
14
  export function createCombatSubsystem(d) {
11
- const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, localUserId, } = d;
15
+ const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, userIdOf, rng, } = d;
12
16
  const death = createDeathSystem({
13
- resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
17
+ resolveOnDeath(instanceId, reason) {
18
+ const spec = catalogEntry(instanceId)?.onDeath;
19
+ if (reason !== undefined && reason.kind !== "player_kill" && allowsUnownedWorldDrops(spec)) {
20
+ // Explicit opt-in must not enable neighboring legacy/unfiltered tables.
21
+ return {
22
+ ...spec,
23
+ drops: Array.isArray(spec?.drops)
24
+ ? spec.drops.filter((rule) => rule.when?.reason === "any") : [],
25
+ };
26
+ }
27
+ return spec;
28
+ },
14
29
  resolveIdentity(instanceId) {
15
30
  const entity = entities.get(instanceId);
16
31
  if (entity === null)
17
32
  return null;
33
+ const userId = userIdOf(instanceId);
18
34
  return {
19
35
  catalogId: entity.name,
20
36
  position: [entity.position[0], entity.position[1], entity.position[2]],
37
+ ...(userId === undefined ? {} : { userId }),
21
38
  };
22
39
  },
23
40
  loot: { roll: (tableId) => (lootRegistry.has(tableId) ? lootRegistry.roll(tableId) : []) },
24
41
  events,
25
- runCommand(name, args) {
26
- runCommand(name, args);
42
+ runCommand(name, args, reason) {
43
+ runCommand(name, args, reason?.kind === "player_kill" ? reason.killerUserId : undefined);
27
44
  },
28
45
  despawn(instanceId) {
29
46
  despawnEntity(instanceId);
@@ -39,7 +56,8 @@ export function createCombatSubsystem(d) {
39
56
  if (entity === null)
40
57
  return null;
41
58
  // `entity.name` is the spawn kind/catalog id, matching the `entity.died` event.
42
- return { catalogId: entity.name, name: entity.name };
59
+ const userId = userIdOf(instanceId);
60
+ return { catalogId: entity.name, name: entity.name, ...(userId === undefined ? {} : { userId }) };
43
61
  },
44
62
  onLethal(instanceId, lethalCtx) {
45
63
  // Capture identity + onDeath *before* resolveDeath despawns the entity.
@@ -49,21 +67,22 @@ export function createCombatSubsystem(d) {
49
67
  const onDeath = catalogEntry(instanceId)?.onDeath;
50
68
  const reason = deathReasonFromEffect({
51
69
  ...lethalCtx,
52
- userIdOf: (id) => (id === localUserId ? localUserId : undefined),
70
+ userIdOf,
53
71
  });
54
72
  const resolution = death.resolveDeath(instanceId, reason);
55
73
  if (resolution.status !== "resolved")
56
74
  return;
57
75
  applyLethalLoot({
58
76
  drops: resolution.drops,
59
- grantToLocalPlayer: isLocalPlayerKill(lethalCtx, localUserId),
77
+ recipientUserId: reason.kind === "player_kill" ? reason.killerUserId : undefined,
78
+ allowUnownedWorldDrops: reason.kind !== "player_kill" && allowsUnownedWorldDrops(onDeath),
60
79
  onDeath,
61
80
  position,
62
81
  catalogId,
63
82
  content,
64
83
  spawnWorldItem,
65
84
  grantToPlayer: loot.grantToPlayer,
66
- localUserId,
85
+ rng,
67
86
  });
68
87
  },
69
88
  }), ["applyEffect"], signalNotify);
@@ -98,7 +117,8 @@ export function createCombatSubsystem(d) {
98
117
  return sceneRaycast.raycastAll({ ...input, filter: projectileFilterFor(input.filter) });
99
118
  },
100
119
  };
101
- const projectiles = notifyAfter(createProjectileSystem({
120
+ const travelQueries = createProjectileTravelQueries(d);
121
+ const projectileOwner = createProjectileSystem({
102
122
  effects: floatingEffects,
103
123
  spatial: combatSpatial,
104
124
  getStat: weapon.getStat,
@@ -117,8 +137,12 @@ export function createCombatSubsystem(d) {
117
137
  entityCollidersOf,
118
138
  rotationYOf: (instanceId) => entities.get(instanceId)?.rotationY,
119
139
  now,
140
+ rng,
141
+ travel: { ...d.projectileTravel, now: time.now, sweep: d.projectileTravel?.sweep ?? travelQueries.sweep },
120
142
  onSettle(report) {
121
143
  events.emit("projectile.settled", {
144
+ shotId: report.shotId,
145
+ hits: structuredClone(report.hits),
122
146
  from: report.from,
123
147
  origin: [report.origin[0], report.origin[1], report.origin[2]],
124
148
  at: [report.at[0], report.at[1], report.at[2]],
@@ -127,7 +151,15 @@ export function createCombatSubsystem(d) {
127
151
  ballistic: report.ballistic,
128
152
  });
129
153
  },
130
- }), ["fireProjectile", "settleProjectile"], signalNotify);
154
+ });
155
+ const resettingProjectileOwner = {
156
+ ...projectileOwner,
157
+ restore(state) {
158
+ projectileOwner.restore(state);
159
+ travelQueries.reset();
160
+ },
161
+ };
162
+ const projectiles = notifyAfter(resettingProjectileOwner, ["fireProjectile", "settleProjectile", "restore"], signalNotify);
131
163
  return {
132
164
  death,
133
165
  effects,
@@ -135,5 +167,141 @@ export function createCombatSubsystem(d) {
135
167
  projectiles,
136
168
  combatFx,
137
169
  vfxInstances,
170
+ captureProjectileTargets(gameDt) { travelQueries.capture(gameDt, projectiles.activeProjectiles().length > 0); },
171
+ };
172
+ }
173
+ /** Derived collision scratch data is rebuilt from the scene each authoritative step. */
174
+ function createProjectileTravelQueries(d) {
175
+ const { entities, time, sceneRaycast, entityCollidersOf } = d;
176
+ const projectileObstacles = d.physics?.projectileObstacles === true;
177
+ const previousTargets = new Map();
178
+ const maxTargets = d.projectileTravel?.maxTargets ?? 2048;
179
+ if (!Number.isInteger(maxTargets) || maxTargets <= 0)
180
+ throw new RangeError("Projectile collision budget must be a positive integer");
181
+ const targetIndex = createSpatialIndex();
182
+ const targetColliders = new Map();
183
+ const targetCandidates = [];
184
+ let targetIndexReady = false;
185
+ let targetStepStart = 0;
186
+ let targetStepDuration = 0;
187
+ function captureProjectileTargets(gameDt, active) {
188
+ previousTargets.clear();
189
+ targetIndexReady = false;
190
+ targetStepDuration = gameDt;
191
+ targetStepStart = time.now() - gameDt;
192
+ if (!active)
193
+ return;
194
+ const ids = d.projectileTravel?.targets?.() ?? entities.ids();
195
+ if (ids.length > maxTargets)
196
+ throw new RangeError("Projectile collision target budget exhausted; provide a bounded target source");
197
+ for (const id of ids) {
198
+ const entity = entities.get(id);
199
+ if (entity !== null)
200
+ previousTargets.set(id, { position: [...entity.position], rotationY: entity.rotationY });
201
+ }
202
+ }
203
+ function indexProjectileTargets() {
204
+ if (targetIndexReady)
205
+ return;
206
+ targetIndex.clear();
207
+ targetColliders.clear();
208
+ const ids = d.projectileTravel?.targets?.() ?? entities.ids();
209
+ if (ids.length > maxTargets)
210
+ throw new RangeError("Projectile collision target budget exhausted; provide a bounded target source");
211
+ for (const id of ids) {
212
+ const entity = entities.get(id);
213
+ if (entity === null)
214
+ continue;
215
+ const previous = previousTargets.get(id);
216
+ const colliders = resolveColliders(entityCollidersOf(id) ?? defaultEntityColliders());
217
+ targetColliders.set(id, colliders);
218
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
219
+ for (const collider of colliders) {
220
+ const start = colliderBounds(collider, previous?.position ?? entity.position, previous?.rotationY ?? entity.rotationY);
221
+ const end = colliderBounds(collider, entity.position, entity.rotationY);
222
+ minX = Math.min(minX, start.min[0], end.min[0]);
223
+ minY = Math.min(minY, start.min[1], end.min[1]);
224
+ minZ = Math.min(minZ, start.min[2], end.min[2]);
225
+ maxX = Math.max(maxX, start.max[0], end.max[0]);
226
+ maxY = Math.max(maxY, start.max[1], end.max[1]);
227
+ maxZ = Math.max(maxZ, start.max[2], end.max[2]);
228
+ }
229
+ if (colliders.length > 0)
230
+ targetIndex.insert(id, { minX, minY, minZ, maxX, maxY, maxZ, centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2, centerZ: (minZ + maxZ) / 2, radius: Math.hypot(maxX - minX, maxY - minY, maxZ - minZ) / 2 });
231
+ }
232
+ targetIndexReady = true;
233
+ }
234
+ function sweepProjectile(from, to, step) {
235
+ if (projectileObstacles && step.radius > 0)
236
+ throw new RangeError("Authored cover requires centerline projectiles (radius 0); inject a radius-aware travel sweep for larger projectiles");
237
+ const distance = Math.hypot(to[0] - from[0], to[1] - from[1], to[2] - from[2]);
238
+ const staticHits = distance > 0 ? sceneRaycast.raycastAll({ origin: from, direction: [to[0] - from[0], to[1] - from[1], to[2] - from[2]], maxDistance: distance, excludeInstanceIds: [step.input.from], filter: { entities: false, objects: projectileObstacles, walls: projectileObstacles, terrain: projectileObstacles } }) : [];
239
+ const blocker = staticHits.find(hit => hit.blocks);
240
+ let nearest = blocker === undefined ? null : { fraction: blocker.distance / distance, at: blocker.point };
241
+ let nearestKey = blocker === undefined ? "" : `${blocker.instanceId}:${blocker.colliderName}`;
242
+ indexProjectileTargets();
243
+ const radius = step.radius;
244
+ const ids = targetIndex.queryBox(Math.min(from[0], to[0]) - radius, Math.min(from[1], to[1]) - radius, Math.min(from[2], to[2]) - radius, Math.max(from[0], to[0]) + radius, Math.max(from[1], to[1]) + radius, Math.max(from[2], to[2]) + radius, targetCandidates);
245
+ for (const id of ids) {
246
+ if (id === step.input.from)
247
+ continue;
248
+ const entity = entities.get(id);
249
+ if (entity === null)
250
+ continue;
251
+ const previous = previousTargets.get(id);
252
+ const fractionAt = (at) => targetStepDuration > 0 ? Math.max(0, Math.min(1, (at - targetStepStart) / targetStepDuration)) : 1;
253
+ const positionAt = (at) => {
254
+ if (previous === undefined)
255
+ return entity.position;
256
+ const alpha = fractionAt(at);
257
+ return [previous.position[0] + (entity.position[0] - previous.position[0]) * alpha, previous.position[1] + (entity.position[1] - previous.position[1]) * alpha, previous.position[2] + (entity.position[2] - previous.position[2]) * alpha];
258
+ };
259
+ const colliders = targetColliders.get(id);
260
+ for (const collider of colliders) {
261
+ if (!collider.blocks && !collider.damageEligible)
262
+ continue;
263
+ const targetFrom = colliderWorldCenter(collider, positionAt(step.fromTime), previous?.rotationY ?? entity.rotationY);
264
+ const targetTo = colliderWorldCenter(collider, positionAt(step.toTime), entity.rotationY);
265
+ let fraction;
266
+ if (collider.shape.kind === "sphere") {
267
+ fraction = sweepMovingSphere(from, to, targetFrom, targetTo, collider.shape.radius + step.radius);
268
+ }
269
+ else if (collider.shape.kind === "mesh" && step.radius === 0) {
270
+ const start = positionAt(step.fromTime);
271
+ const end = positionAt(step.toTime);
272
+ const origin = [from[0] + end[0] - start[0], from[1] + end[1] - start[1], from[2] + end[2] - start[2]];
273
+ const motion = [to[0] - origin[0], to[1] - origin[1], to[2] - origin[2]];
274
+ const length = Math.hypot(...motion);
275
+ const hit = length > 0 ? raycastCollisionMesh(collider.shape.mesh, origin, [motion[0] / length, motion[1] / length, motion[2] / length], length, end, entity.rotationY, collider.shape.meshScale, collider.shape.meshTranslate) : null;
276
+ fraction = hit === null ? null : hit.distance / length;
277
+ }
278
+ else {
279
+ const half = collider.shape.halfExtents;
280
+ fraction = sweepMovingBounds(from, to, targetFrom, targetTo, [half[0] + step.radius, half[1] + step.radius, half[2] + step.radius]);
281
+ }
282
+ const key = `${id}:${collider.name}`;
283
+ if (fraction === null || (nearest !== null && (fraction > nearest.fraction || (fraction === nearest.fraction && key >= nearestKey))))
284
+ continue;
285
+ nearestKey = key;
286
+ nearest = {
287
+ fraction, at: [from[0] + (to[0] - from[0]) * fraction, from[1] + (to[1] - from[1]) * fraction, from[2] + (to[2] - from[2]) * fraction],
288
+ target: { kind: "entity", instanceId: id, distance: distance * fraction, colliderName: collider.name, damageEligible: collider.damageEligible },
289
+ };
290
+ }
291
+ }
292
+ return nearest;
293
+ }
294
+ return {
295
+ capture: captureProjectileTargets,
296
+ sweep: sweepProjectile,
297
+ reset() {
298
+ previousTargets.clear();
299
+ targetIndex.clear();
300
+ targetColliders.clear();
301
+ targetCandidates.length = 0;
302
+ targetIndexReady = false;
303
+ targetStepStart = 0;
304
+ targetStepDuration = 0;
305
+ },
138
306
  };
139
307
  }
@@ -1,4 +1,4 @@
1
- import { type EffectDeathContext, type OnDeathSpec } from "../../combat/death.js";
1
+ import { type OnDeathSpec } from "../../combat/death.js";
2
2
  import type { Drop } from "../../game/lootTable.js";
3
3
  import { type WorldItemSpawnInput } from "../../game/worldItem.js";
4
4
  import type { GameContextContent } from "../gameContext.js";
@@ -6,8 +6,10 @@ import type { GameContextContent } from "../gameContext.js";
6
6
  export interface LethalLootInput {
7
7
  /** Drops produced by the death system's resolution (already rolled). */
8
8
  drops: readonly Drop[];
9
- /** True when the killer is the local/host player this context is resolving loot for. */
10
- grantToLocalPlayer: boolean;
9
+ /** Resolved killer identity; environmental kills have no player recipient. */
10
+ recipientUserId: string | undefined;
11
+ /** Explicit any-death world rules may spawn items without a player recipient. */
12
+ allowUnownedWorldDrops?: boolean;
11
13
  /** Catalog `onDeath` for the dying entity (drop mode + scatter). */
12
14
  onDeath: OnDeathSpec | undefined;
13
15
  /** World position of the dying entity — required for `dropMode: "world"`. */
@@ -17,14 +19,15 @@ export interface LethalLootInput {
17
19
  content: GameContextContent;
18
20
  spawnWorldItem: (input: WorldItemSpawnInput) => void;
19
21
  grantToPlayer: (userId: string, drops: Drop[], source?: string) => void;
20
- localUserId: string;
22
+ /** The world's seeded stream — scatter positions must replay with the rest of the simulation. */
23
+ rng: () => number;
21
24
  }
25
+ /** @internal Only an explicit rule opts a catalog into unowned world drops. */
26
+ export declare function allowsUnownedWorldDrops(onDeath: OnDeathSpec | undefined): boolean;
22
27
  /**
23
- * Pure death→loot policy: when a kill yields drops for the local player, either scatter them as
24
- * world items (`dropMode: "world"`) or grant straight into bags. Extracted from `createGameContext`
28
+ * Pure death→loot policy: scatter world items or grant straight into player bags.
29
+ * Unowned deaths require explicit opt-in and never grant currency. Extracted from `createGameContext`
25
30
  * so combat install stays free of nested loot branching.
26
31
  * @internal
27
32
  */
28
33
  export declare function applyLethalLoot(input: LethalLootInput): void;
29
- /** @internal Build the grant-to-local-player flag from a lethal effect context + local user id. */
30
- export declare function isLocalPlayerKill(lethalCtx: EffectDeathContext, localUserId: string): boolean;
@@ -1,19 +1,28 @@
1
- import { deathReasonFromEffect, normalizeOnDeath, } from "../../combat/death.js";
1
+ import { normalizeOnDeath } from "../../combat/death.js";
2
2
  import { DEFAULT_RARITY, resolveDeathDrops, } from "../../game/worldItem.js";
3
+ /** @internal Only an explicit rule opts a catalog into unowned world drops. */
4
+ export function allowsUnownedWorldDrops(onDeath) {
5
+ const normalized = normalizeOnDeath(onDeath);
6
+ return normalized.dropMode === "world" && normalized.drops.some((rule) => rule.when?.reason === "any");
7
+ }
3
8
  /**
4
- * Pure death→loot policy: when a kill yields drops for the local player, either scatter them as
5
- * world items (`dropMode: "world"`) or grant straight into bags. Extracted from `createGameContext`
9
+ * Pure death→loot policy: scatter world items or grant straight into player bags.
10
+ * Unowned deaths require explicit opt-in and never grant currency. Extracted from `createGameContext`
6
11
  * so combat install stays free of nested loot branching.
7
12
  * @internal
8
13
  */
9
14
  export function applyLethalLoot(input) {
10
- if (input.drops.length === 0 || !input.grantToLocalPlayer)
15
+ if (input.drops.length === 0)
11
16
  return;
12
17
  const normalizedOnDeath = normalizeOnDeath(input.onDeath);
18
+ if (input.recipientUserId === undefined &&
19
+ !(input.allowUnownedWorldDrops && normalizedOnDeath.dropMode === "world" && input.position !== undefined))
20
+ return;
13
21
  if (normalizedOnDeath.dropMode === "world" && input.position !== undefined) {
14
22
  const resolved = resolveDeathDrops([...input.drops], {
15
23
  mode: "world",
16
24
  origin: input.position,
25
+ rng: input.rng,
17
26
  resolveRarity: (itemId) => input.content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
18
27
  resolveBaseType: (itemId) => input.content.itemById?.(itemId)?.baseType ?? itemId,
19
28
  scatter: normalizedOnDeath.scatter,
@@ -21,19 +30,11 @@ export function applyLethalLoot(input) {
21
30
  });
22
31
  for (const spawn of resolved.worldSpawns)
23
32
  input.spawnWorldItem(spawn);
24
- if (resolved.grants.length > 0) {
25
- input.grantToPlayer(input.localUserId, resolved.grants, input.catalogId);
33
+ if (resolved.grants.length > 0 && input.recipientUserId !== undefined) {
34
+ input.grantToPlayer(input.recipientUserId, resolved.grants, input.catalogId);
26
35
  }
27
36
  }
28
- else {
29
- input.grantToPlayer(input.localUserId, [...input.drops], input.catalogId);
37
+ else if (input.recipientUserId !== undefined) {
38
+ input.grantToPlayer(input.recipientUserId, [...input.drops], input.catalogId);
30
39
  }
31
40
  }
32
- /** @internal Build the grant-to-local-player flag from a lethal effect context + local user id. */
33
- export function isLocalPlayerKill(lethalCtx, localUserId) {
34
- const reason = deathReasonFromEffect({
35
- ...lethalCtx,
36
- userIdOf: (id) => (id === localUserId ? localUserId : undefined),
37
- });
38
- return reason.kind === "player_kill" && reason.killerUserId === localUserId;
39
- }
@@ -13,6 +13,7 @@ import { type SceneRaycastApi } from "../../scene/sceneRaycast.js";
13
13
  import { type SpatialApi } from "../../scene/spatial.js";
14
14
  import { type Targeting } from "../../scene/targeting.js";
15
15
  import type { TerrainField } from "../../world/terrain.js";
16
+ import type { WorldSolids } from "../../world/worldSolids.js";
16
17
  import type { SimClock } from "../../time/simClock.js";
17
18
  import type { GameContextContent, GameContextEntityEntry, GameContextModels, GameContextObjectEntry, MoveTowardCommitOptions, SceneObjectContext } from "../gameContext.js";
18
19
  /** @internal Live scene stores + spatial/collider/raycast wiring handed in by `createGameContext`. */
@@ -20,6 +21,7 @@ export interface SceneSubsystemDeps {
20
21
  content: GameContextContent;
21
22
  signalNotify: () => void;
22
23
  ground: TerrainField;
24
+ solids?: Pick<WorldSolids, "count" | "inBox">;
23
25
  events: GameEvents;
24
26
  /** Full sim clock — forms need `after` for timed shapeshift reversion. */
25
27
  time: SimClock;
@@ -18,7 +18,14 @@ import { notifyAfter } from "../../store/changeSignal.js";
18
18
  /** @internal */
19
19
  export function createSceneSubsystem(d) {
20
20
  const { content, signalNotify, ground, events, time, occluder } = d;
21
- const entities = createEntityStore();
21
+ const entities = createEntityStore({
22
+ onChange: (instanceId) => {
23
+ if (instanceId === undefined)
24
+ spatial.invalidate();
25
+ else
26
+ spatial.updateEntity(instanceId);
27
+ },
28
+ });
22
29
  const objects = createObjectStore();
23
30
  entities.subscribe(signalNotify);
24
31
  objects.subscribe(signalNotify);
@@ -40,39 +47,23 @@ export function createSceneSubsystem(d) {
40
47
  const object = objects.get(instanceId);
41
48
  return object === null ? undefined : content.objectById?.(object.catalogId);
42
49
  }
43
- let spatialGeneration = 0;
44
- const candidateIds = [];
45
- let candidateIdsDirty = true;
46
- function refreshCandidateIds() {
47
- if (!candidateIdsDirty)
48
- return candidateIds;
49
- candidateIds.length = 0;
50
- for (const entity of entities.list())
51
- candidateIds.push(entity.id);
52
- candidateIdsDirty = false;
53
- return candidateIds;
54
- }
55
50
  // `moveToward` reads the same blocking colliders the player resolver does, so a chaser stops at the
56
51
  // wall that stops the player rather than lerping through it.
57
52
  const walkerReach = createObstacleReachCache();
58
53
  const solidSource = {
54
+ solids: d.solids,
59
55
  list: () => objects.list(),
60
56
  inBox: (min, max) => objects.inBox(min, max),
61
57
  collidersOf: (instanceId) => objectCollidersOf(instanceId),
62
58
  };
63
59
  const spatial = createSpatialApi({
64
60
  resolvePosition: (instanceId) => entities.get(instanceId)?.position,
65
- candidates: refreshCandidateIds,
61
+ candidates: entities.ids,
66
62
  grid: { cellSize: 8 },
67
- getVersion: () => spatialGeneration,
63
+ incremental: true,
68
64
  resolveStep: (position, stepX, stepZ) => slideStep(position, stepX, stepZ, sourceObstaclesNear(solidSource, sourceObstacleReach(solidSource, walkerReach), position, Math.abs(stepX) + DEFAULT_OBSTACLE_PLAYER_RADIUS, Math.abs(stepZ) + DEFAULT_OBSTACLE_PLAYER_RADIUS)),
69
65
  ...(occluder !== undefined ? { occluder } : {}),
70
66
  });
71
- entities.subscribe(() => {
72
- candidateIdsDirty = true;
73
- spatialGeneration += 1;
74
- spatial.invalidate();
75
- });
76
67
  const entityColliders = new Map();
77
68
  const objectColliders = new Map();
78
69
  const fittedEntityByKind = new Map();
@@ -216,7 +207,7 @@ export function createSceneSubsystem(d) {
216
207
  return scaledObjectColliders(objectVisualScale(object.visual));
217
208
  }
218
209
  const targeting = notifyAfter(createTargeting({
219
- candidates: () => [...refreshCandidateIds()],
210
+ candidates: () => [...entities.ids()],
220
211
  classify(_fromId, toId) {
221
212
  const role = catalogEntry(toId)?.role;
222
213
  return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
@@ -238,6 +229,7 @@ export function createSceneSubsystem(d) {
238
229
  positionOf: (instanceId) => entities.get(instanceId)?.position,
239
230
  };
240
231
  const sceneRaycast = createSceneRaycast({
232
+ solids: d.solids,
241
233
  entities: {
242
234
  list: () => entities.list().map((entity) => ({
243
235
  id: entity.id,
@@ -1,5 +1,31 @@
1
1
  import { hydrateEntityStats, snapshotEntityStats, } from "../../scene/entityStats.js";
2
2
  import { decodeArray, decodeEntries, decodeRecord } from "../context/snapshotCodecs.js";
3
+ // A store slot whose value carries functions (a closure-based run store, an input handle) is that
4
+ // world's runtime object, not world state: it cannot be cloned, persisted or sent, so it stays out of
5
+ // snapshots and survives hydration in place.
6
+ function holdsLiveCode(value, seen = new Set()) {
7
+ if (typeof value === "function")
8
+ return true;
9
+ if (value === null || typeof value !== "object" || seen.has(value) || ArrayBuffer.isView(value))
10
+ return false;
11
+ seen.add(value);
12
+ if (value instanceof Map) {
13
+ for (const [key, entry] of value)
14
+ if (holdsLiveCode(key, seen) || holdsLiveCode(entry, seen))
15
+ return true;
16
+ return false;
17
+ }
18
+ if (value instanceof Set) {
19
+ for (const entry of value)
20
+ if (holdsLiveCode(entry, seen))
21
+ return true;
22
+ return false;
23
+ }
24
+ for (const key in value)
25
+ if (holdsLiveCode(value[key], seen))
26
+ return true;
27
+ return false;
28
+ }
3
29
  /** @internal */
4
30
  export const baselineDescriptors = [
5
31
  {
@@ -29,9 +55,14 @@ export const baselineDescriptors = [
29
55
  create: (d) => ({
30
56
  replicate: {
31
57
  key: "store",
32
- snapshot: () => d.store.snapshot(),
58
+ snapshot: () => d.store.snapshot().filter(([, value]) => !holdsLiveCode(value)),
33
59
  decode: (raw) => decodeEntries(raw),
34
- hydrate: (data) => d.store.hydrate(data),
60
+ hydrate: (data) => {
61
+ const incoming = data;
62
+ const incomingKeys = new Set(incoming.map(([key]) => key));
63
+ const localLive = d.store.snapshot().filter(([key, value]) => !incomingKeys.has(key) && holdsLiveCode(value));
64
+ d.store.hydrate(localLive.length === 0 ? incoming : [...incoming, ...localLive]);
65
+ },
35
66
  },
36
67
  }),
37
68
  },
@@ -94,7 +125,7 @@ export const baselineDescriptors = [
94
125
  {
95
126
  key: "time",
96
127
  create: (d) => ({
97
- save: {
128
+ replicate: {
98
129
  key: "time",
99
130
  snapshot: () => d.time.snapshot(),
100
131
  decode: (raw) => {
@@ -4,7 +4,7 @@ import { type WalletState } from "../../economy/wallet.js";
4
4
  import type { GameFeatures } from "../../game/defineGame.js";
5
5
  import type { GameEvents } from "../../game/events.js";
6
6
  import { type Social } from "../../game/social.js";
7
- import type { InventoryLayout, InventorySet } from "../../inventory/inventoryModel.js";
7
+ import { type InventoryLayout, type InventorySet, type ItemTraits } from "../../inventory/inventoryModel.js";
8
8
  import type { StatValueMap } from "../../scene/entityStats.js";
9
9
  import type { EntityStore } from "../../scene/entityStore.js";
10
10
  import type { SpatialApi } from "../../scene/spatial.js";
@@ -35,6 +35,7 @@ export interface FeatureDeps {
35
35
  walletOf: (userId: string) => WalletState;
36
36
  setWallet: (userId: string, state: WalletState) => void;
37
37
  layouts: Record<string, InventoryLayout>;
38
+ itemTraits: ItemTraits;
38
39
  inventoryFor: (userId: string) => InventorySet<string>;
39
40
  ensureInstanceStats: (instanceId: string) => StatValueMap;
40
41
  seedUserPool: (userId: string, statId: string, pool: {
@@ -7,6 +7,7 @@ import { createChat } from "../../game/chat.js";
7
7
  import {} from "../../game/social.js";
8
8
  import { createTradeSystem } from "../../game/trade.js";
9
9
  import { createUnlocks } from "../../game/unlocks.js";
10
+ import { putItem, } from "../../inventory/inventoryModel.js";
10
11
  import { createRoster } from "../../scene/roster.js";
11
12
  import { createConnectedPlayers } from "../../game/connectedPlayers.js";
12
13
  import { notifyAfter } from "../../store/changeSignal.js";
@@ -197,6 +198,23 @@ export const featureDescriptors = [
197
198
  const result = d.inventoryFor(userId).put(inventoryId, itemId, count);
198
199
  return result.status === "ok" ? null : { reason: result.reason };
199
200
  },
201
+ grantItems(userId, items) {
202
+ const inventories = d.inventoryFor(userId);
203
+ const staged = new Map();
204
+ for (const entry of items) {
205
+ const layout = d.layouts[entry.inventory];
206
+ if (layout === undefined)
207
+ return { reason: `unknown inventory "${entry.inventory}"` };
208
+ const current = staged.get(entry.inventory) ?? inventories.state(entry.inventory);
209
+ const result = putItem(current, layout, d.itemTraits, entry.item, entry.count);
210
+ if (result.status !== "ok")
211
+ return { reason: result.reason };
212
+ staged.set(entry.inventory, result.state);
213
+ }
214
+ for (const [inventoryId, state] of staged)
215
+ inventories.replaceState(inventoryId, state);
216
+ return null;
217
+ },
200
218
  grantUnlock: (userId, unlockId) => d.feature("unlocks")?.grant(userId, unlockId),
201
219
  },
202
220
  hasUnlock: (userId, id) => d.feature("unlocks")?.has(userId, id) ?? false,