@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
@@ -0,0 +1,42 @@
1
+ /** Inject authoritative spatial forces before existing movement consumes its motion intents.
2
+ * @capability environment-motion apply capped spatial wind and vortex forces to game-selected actors through shared motion intents
3
+ */
4
+ export function installEnvironmentMotion(ctx, options) {
5
+ const limit = options.maxTargets ?? 64;
6
+ if (!Number.isInteger(limit) || limit < 1 || limit > 4096)
7
+ throw new Error("environment motion target budget must be an integer in 1..4096");
8
+ const seen = new Set();
9
+ const pending = [];
10
+ return ctx.sim.addStage({
11
+ id: "environment-motion",
12
+ phase: "beforeMovement",
13
+ run(_realDt, _tick, gameDt) {
14
+ if (gameDt <= 0)
15
+ return;
16
+ const targets = options.targets();
17
+ if (targets.length > limit)
18
+ throw new Error("environment motion target budget exceeded");
19
+ seen.clear();
20
+ pending.length = 0;
21
+ for (const target of targets) {
22
+ if (!target || typeof target.entityId !== "string" || target.entityId.length === 0 || typeof target.motion?.pushHorizontal !== "function" || typeof target.motion?.impulse !== "function")
23
+ throw new Error("environment motion target requires an entity id and motion sink");
24
+ if (seen.has(target.entityId))
25
+ throw new Error(`duplicate environment motion target ${target.entityId}`);
26
+ seen.add(target.entityId);
27
+ const entity = ctx.scene.entity.get(target.entityId);
28
+ if (entity === null)
29
+ continue;
30
+ const acceleration = ctx.environment.accelerationAt(entity.position, ctx.time.now(), target.windResponse, target.maxAcceleration, target.mask ?? 0);
31
+ const exposure = target.sheltered === true ? ctx.environment.exposureAt(...entity.position) : 1;
32
+ pending.push({ target, acceleration, exposure });
33
+ }
34
+ for (const { target, acceleration, exposure } of pending) {
35
+ if (acceleration[0] !== 0 || acceleration[2] !== 0)
36
+ target.motion.pushHorizontal(acceleration[0] * gameDt * exposure, acceleration[2] * gameDt * exposure);
37
+ if (acceleration[1] !== 0)
38
+ target.motion.impulse(acceleration[1] * gameDt * exposure);
39
+ }
40
+ },
41
+ });
42
+ }
@@ -66,13 +66,13 @@ export interface TerrainDetailConfig {
66
66
  * PBR map URLs for a real ground texture — the same shape
67
67
  * `buildMaterialCatalog({ basePath }).resolve(id)!.maps` from `@jgengine/assets` returns. Kept
68
68
  * dependency-free here so `core` never imports the assets package; any URLs (pulled maps, a CDN, a
69
- * data URI) satisfy it.
69
+ * data URI) satisfy it. Omitted roughness keeps the terrain scalar; omitted AO skips occlusion sampling.
70
70
  */
71
71
  export interface TerrainMaterialMaps {
72
72
  color: string;
73
73
  normal: string;
74
- roughness: string;
75
- ao: string;
74
+ roughness?: string;
75
+ ao?: string;
76
76
  displacement: string;
77
77
  }
78
78
  /**
@@ -296,16 +296,38 @@ export interface OceanEnvironmentConfig {
296
296
  waveSpeed?: number;
297
297
  color?: string;
298
298
  }
299
+ /**
300
+ * Where the sun sits, as a compass bearing and a height above the horizon, both in degrees.
301
+ * `azimuth` 0 points toward -Z (north), 90 toward +X (east); `elevation` 90 is straight overhead.
302
+ */
303
+ export interface SkySunConfig {
304
+ /** Compass bearing of the sun in degrees; 0 = -Z, 90 = +X. Default 0. */
305
+ azimuth?: number;
306
+ /** Height above the horizon in degrees; 0 sits on the horizon, 90 is overhead. Default 68. */
307
+ elevation?: number;
308
+ }
299
309
  export interface SkyEnvironmentConfig {
300
310
  /** Fixed look used when `timeOfDay` is off (or no clock is available); default "day". */
301
311
  preset?: "day" | "dusk" | "night";
302
312
  /** Drive sun/sky from the world clock's `calendar().dayFraction` instead of the fixed `preset`. */
303
313
  timeOfDay?: boolean;
314
+ /**
315
+ * Horizon tint at noon. Under `timeOfDay` this only sets the noon keyframe; dawn, dusk, and night
316
+ * still crossfade through the engine presets, so an authored tint is visible for part of the day.
317
+ */
304
318
  horizonColor?: string;
319
+ /** Zenith tint at noon; same `timeOfDay` crossfade caveat as `horizonColor`. */
305
320
  zenithColor?: string;
306
321
  sunIntensity?: number;
307
322
  ambientIntensity?: number;
308
- /** Sky-dome sphere radius in world units. Enlarge for a playfield sited far from the origin so the viewer never exits the dome. Default 260. */
323
+ /**
324
+ * Sun placement for the dome glow and the sky-owned sun light. With a fixed `preset` this is the
325
+ * sun's exact direction; under `timeOfDay` it is where the sun stands at noon and the day arc
326
+ * swings around that bearing. Unset with an authored `lighting.directional` light, the shell takes
327
+ * the first directional light's direction so the dome sun agrees with the shadows.
328
+ */
329
+ sun?: SkySunConfig;
330
+ /** Sky-dome sphere radius in world units. Enlarge for a playfield sited far from the origin so the viewer never exits the dome. Must stay below the camera far plane or the dome depth-clips to black. Default 260. */
309
331
  radius?: number;
310
332
  /** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
311
333
  hazeStrength?: number;
@@ -377,6 +399,11 @@ export interface BuildingEnvironmentConfig {
377
399
  * untextured block; without a kit the whole facade stays blocks, as before.
378
400
  */
379
401
  kit?: BuildingKit;
402
+ /**
403
+ * Whether the buildings block movement and physics through `ctx.world.solids` (default `true`).
404
+ * `false` keeps them render-only, e.g. a distant skyline nobody can reach.
405
+ */
406
+ solid?: boolean;
380
407
  }
381
408
  export type PadSize = readonly [number, number] | {
382
409
  radius: number;
@@ -418,7 +445,7 @@ export type OceanEnvironmentDescriptor = {
418
445
  } & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position" | "levelAt">;
419
446
  export type BuildingEnvironmentDescriptor = {
420
447
  kind: "building";
421
- } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit">;
448
+ } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit" | "solid">;
422
449
  export type PadEnvironmentDescriptor = {
423
450
  kind: "pad";
424
451
  } & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY" | "elevation">;
@@ -1,3 +1,4 @@
1
+ import { warnOnce } from "../devtools/warnOnce.js";
1
2
  function list(value) {
2
3
  if (value === undefined) {
3
4
  return undefined;
@@ -110,6 +111,9 @@ export function terrain(config = {}) {
110
111
  });
111
112
  }
112
113
  export function sky(config = {}) {
114
+ if (config.timeOfDay === true && (config.zenithColor !== undefined || config.horizonColor !== undefined)) {
115
+ warnOnce("sky-timeofday-tint", "[jgengine] sky(): zenithColor/horizonColor only set the noon keyframe under timeOfDay; dawn, dusk, and night crossfade through the engine presets. Drop timeOfDay for a fixed tint.");
116
+ }
113
117
  return withOptional({
114
118
  kind: "sky",
115
119
  preset: config.preset ?? "day",
@@ -119,6 +123,7 @@ export function sky(config = {}) {
119
123
  ...(config.zenithColor === undefined ? {} : { zenithColor: config.zenithColor }),
120
124
  ...(config.sunIntensity === undefined ? {} : { sunIntensity: config.sunIntensity }),
121
125
  ...(config.ambientIntensity === undefined ? {} : { ambientIntensity: config.ambientIntensity }),
126
+ ...(config.sun === undefined ? {} : { sun: { ...config.sun } }),
122
127
  ...(config.radius === undefined ? {} : { radius: config.radius }),
123
128
  ...(config.hazeStrength === undefined ? {} : { hazeStrength: config.hazeStrength }),
124
129
  ...(config.sunGlowStrength === undefined ? {} : { sunGlowStrength: config.sunGlowStrength }),
@@ -253,6 +258,7 @@ export function building(config = {}) {
253
258
  ...(config.palette === undefined ? {} : { palette: config.palette }),
254
259
  ...(config.along === undefined ? {} : { along: config.along }),
255
260
  ...(config.kit === undefined ? {} : { kit: config.kit }),
261
+ ...(config.solid === undefined ? {} : { solid: config.solid }),
256
262
  });
257
263
  }
258
264
  export function pad(config) {
@@ -16,6 +16,10 @@ export interface FootprintReservation {
16
16
  kind: string;
17
17
  cells: readonly GridCell[];
18
18
  }
19
+ /** Plain JSON state of a {@link FootprintGrid}: live reservations in claim order. */
20
+ export interface FootprintGridState {
21
+ reservations: FootprintReservation[];
22
+ }
19
23
  /** Handle returned by {@link createFootprintGrid}. */
20
24
  export interface FootprintGrid {
21
25
  readonly cellSize: number;
@@ -30,6 +34,9 @@ export interface FootprintGrid {
30
34
  reservationOf(id: string): FootprintReservation | null;
31
35
  list(): readonly FootprintReservation[];
32
36
  clear(): void;
37
+ snapshot(): FootprintGridState;
38
+ /** Replaces every reservation with `next`'s and rebuilds cell occupancy from them. */
39
+ restore(next: FootprintGridState): void;
33
40
  }
34
41
  /**
35
42
  * Multi-cell footprint occupancy/reservation on a shared build grid — `world/placementController`
@@ -55,5 +62,60 @@ export declare function boundaryNeighbors(grid: FootprintGrid, cells: readonly G
55
62
  * (no occupied neighbors at all) passes unless `requireConnection` demands one.
56
63
  */
57
64
  export declare function hasValidAdjacency(grid: FootprintGrid, cells: readonly GridCell[], accepts: (neighborKind: string) => boolean, requireConnection?: boolean): boolean;
65
+ /** Caller-owned logical footprint. Cells must be unique, safe integer addresses and cardinally connected. */
66
+ export interface RegionFootprint {
67
+ readonly id: string;
68
+ readonly kind: string;
69
+ readonly tier: number;
70
+ readonly cells: readonly GridCell[];
71
+ /** Nonnegative finite contribution; independent of occupied area. */
72
+ readonly capacity: number;
73
+ }
74
+ /** Original footprint identity and capacity retained when regions pool. */
75
+ export interface RegionContribution {
76
+ readonly id: string;
77
+ readonly cells: readonly GridCell[];
78
+ readonly capacity: number;
79
+ }
80
+ /** Plain JSON pooled region. Contributions partition its cells and sum to its capacity. */
81
+ export interface MergedRegion extends RegionFootprint {
82
+ readonly contributions: readonly RegionContribution[];
83
+ }
84
+ /** Optional caller work limits, checked before cell traversal. Omitted limits follow input size. */
85
+ export interface RegionWorkBudget {
86
+ readonly maxCells?: number;
87
+ readonly maxContributions?: number;
88
+ }
89
+ /** Identities for split sides containing multiple contributions; singletons retain their original id. */
90
+ export interface RegionSplitIds {
91
+ readonly left?: string;
92
+ readonly right?: string;
93
+ }
94
+ /** A standalone footprint or a pooled region carrying its original contributions. */
95
+ export type FootprintRegion = RegionFootprint | MergedRegion;
96
+ /** Matching kind/tier, disjoint valid source identities/cells, and cardinal contact. Linear in input size.
97
+ * @capability mergeable-footprint-regions check compatible adjacent regions within caller work budgets
98
+ */
99
+ export declare function canMerge(a: FootprintRegion, b: FootprintRegion, budget?: RegionWorkBudget): boolean;
100
+ /**
101
+ * Pool compatible footprints under caller-owned `id`, retaining original capacities and identities.
102
+ * Returns null for invalid data, kind/tier mismatch, overlap, no cardinal contact or exhausted budget.
103
+ * Does not mutate inputs or grid reservations; costs, merge width and upgrade policy remain caller-owned.
104
+ * Capacity is the ordered JavaScript-number sum of original contributions, independent of cell area.
105
+ *
106
+ * @capability merge-footprint-regions pool adjacent compatible footprints with serializable capacity provenance
107
+ */
108
+ export declare function mergeFootprints(a: FootprintRegion, b: FootprintRegion, id: string, budget?: RegionWorkBudget): MergedRegion | null;
109
+ /**
110
+ * Partition whole original contributions by `leftIds`; both sides must remain cardinally connected.
111
+ * Singletons restore their original id and capacity; larger sides require explicit output identities.
112
+ * Returns null for invalid provenance/budget, unknown or duplicate ids, empty/disconnected sides, or
113
+ * conflicting output identities (including identities from the opposite side's contributions).
114
+ * An output may reuse its own side's source identity. Cell cuts/capacity repricing require caller policy.
115
+ * Both sides inherit the current region kind/tier. Inputs and grid reservations remain unchanged.
116
+ *
117
+ * @capability split-footprint-regions split pooled regions along retained contributions without area-based capacity loss
118
+ */
119
+ export declare function splitRegion(region: MergedRegion, leftIds: readonly string[], ids?: RegionSplitIds, budget?: RegionWorkBudget): [FootprintRegion, FootprintRegion] | null;
58
120
  /** Bridges live reservations into `world/placement`'s `PlacementRules.obstacles` so `validatePlacement`/`createPlacementController` see the grid's committed footprints unchanged. */
59
121
  export declare function footprintObstacles(grid: FootprintGrid): PlacementObstacle[];
@@ -1,6 +1,9 @@
1
1
  function cellKey(cell) {
2
2
  return `${cell.col}:${cell.row}`;
3
3
  }
4
+ function copyReservation(reservation) {
5
+ return { id: reservation.id, kind: reservation.kind, cells: reservation.cells.map((cell) => ({ col: cell.col, row: cell.row })) };
6
+ }
4
7
  /**
5
8
  * Multi-cell footprint occupancy/reservation on a shared build grid — `world/placementController`
6
9
  * only owns the ghost preview; this is the persistent claim a committed placement holds so the next
@@ -70,6 +73,19 @@ export function createFootprintGrid(options = {}) {
70
73
  occupied.clear();
71
74
  reservations.clear();
72
75
  },
76
+ snapshot() {
77
+ return { reservations: Array.from(reservations.values(), copyReservation) };
78
+ },
79
+ restore(next) {
80
+ occupied.clear();
81
+ reservations.clear();
82
+ for (const reservation of next.reservations) {
83
+ const copy = copyReservation(reservation);
84
+ reservations.set(copy.id, copy);
85
+ for (const cell of copy.cells)
86
+ occupied.set(cellKey(cell), copy.id);
87
+ }
88
+ },
73
89
  };
74
90
  }
75
91
  const CARDINAL_OFFSETS = [
@@ -111,6 +127,207 @@ export function hasValidAdjacency(grid, cells, accepts, requireConnection = fals
111
127
  return false;
112
128
  return !requireConnection || neighbors.some((neighbor) => accepts(neighbor.kind));
113
129
  }
130
+ function validId(id) { return typeof id === "string" && id.length > 0; }
131
+ function validCapacity(capacity) { return Number.isFinite(capacity) && capacity >= 0; }
132
+ function contributionCount(region) {
133
+ if (region === null || typeof region !== "object")
134
+ return null;
135
+ if (!Array.isArray(region.cells))
136
+ return null;
137
+ if (!("contributions" in region))
138
+ return 1;
139
+ return Array.isArray(region.contributions) ? region.contributions.length : null;
140
+ }
141
+ function withinRegionBudget(regions, budget) {
142
+ let cells = 0;
143
+ let contributions = 0;
144
+ for (const region of regions) {
145
+ const count = contributionCount(region);
146
+ if (count === null)
147
+ return false;
148
+ cells += region.cells.length;
149
+ contributions += count;
150
+ }
151
+ for (const [limit, count] of [[budget.maxCells, cells], [budget.maxContributions, contributions]]) {
152
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0 || count > limit))
153
+ return false;
154
+ }
155
+ return true;
156
+ }
157
+ function regionCells(cells) {
158
+ if (cells.length === 0)
159
+ return null;
160
+ const out = new Map();
161
+ for (const cell of cells) {
162
+ if (cell === null || typeof cell !== "object")
163
+ return null;
164
+ if (!Number.isSafeInteger(cell.col) || !Number.isSafeInteger(cell.row))
165
+ return null;
166
+ const key = cellKey(cell);
167
+ if (out.has(key))
168
+ return null;
169
+ out.set(key, cell);
170
+ }
171
+ return out;
172
+ }
173
+ function connectedRegion(cells) {
174
+ const first = cells.values().next().value;
175
+ if (first === undefined)
176
+ return false;
177
+ const queue = [first];
178
+ const visited = new Set([cellKey(first)]);
179
+ for (let index = 0; index < queue.length; index++) {
180
+ const cell = queue[index];
181
+ for (const offset of CARDINAL_OFFSETS) {
182
+ const key = cellKey({ col: cell.col + offset.col, row: cell.row + offset.row });
183
+ const next = cells.get(key);
184
+ if (next !== undefined && !visited.has(key)) {
185
+ visited.add(key);
186
+ queue.push(next);
187
+ }
188
+ }
189
+ }
190
+ return queue.length === cells.size;
191
+ }
192
+ function checkedRegion(region) {
193
+ if (!validId(region.id) || !validId(region.kind) || !Number.isFinite(region.tier) || !validCapacity(region.capacity))
194
+ return null;
195
+ const cells = regionCells(region.cells);
196
+ if (cells === null || !connectedRegion(cells))
197
+ return null;
198
+ const contributions = "contributions" in region ? region.contributions : [region];
199
+ if (contributions.length === 0)
200
+ return null;
201
+ const ids = new Set();
202
+ const covered = new Set();
203
+ let capacity = 0;
204
+ for (const contribution of contributions) {
205
+ if (contribution === null || typeof contribution !== "object")
206
+ return null;
207
+ if (!validId(contribution.id) || ids.has(contribution.id) || !validCapacity(contribution.capacity) || !Array.isArray(contribution.cells))
208
+ return null;
209
+ if (contribution.cells.length > cells.size - covered.size)
210
+ return null;
211
+ ids.add(contribution.id);
212
+ const partCells = regionCells(contribution.cells);
213
+ if (partCells === null || !connectedRegion(partCells))
214
+ return null;
215
+ for (const key of partCells.keys()) {
216
+ if (!cells.has(key) || covered.has(key))
217
+ return null;
218
+ covered.add(key);
219
+ }
220
+ capacity += contribution.capacity;
221
+ }
222
+ if (!Number.isFinite(capacity) || capacity !== region.capacity || covered.size !== cells.size)
223
+ return null;
224
+ return { cells, contributions, ids };
225
+ }
226
+ function mergeableRegions(a, b, budget) {
227
+ if (!withinRegionBudget([a, b], budget) || a.id === b.id || a.kind !== b.kind || a.tier !== b.tier)
228
+ return null;
229
+ const left = checkedRegion(a);
230
+ const right = checkedRegion(b);
231
+ if (left === null || right === null)
232
+ return null;
233
+ if (right.ids.has(a.id) || left.ids.has(b.id))
234
+ return null;
235
+ for (const id of left.ids)
236
+ if (right.ids.has(id))
237
+ return null;
238
+ let adjacent = false;
239
+ for (const [key, cell] of left.cells) {
240
+ if (right.cells.has(key))
241
+ return null;
242
+ for (const offset of CARDINAL_OFFSETS) {
243
+ if (right.cells.has(cellKey({ col: cell.col + offset.col, row: cell.row + offset.row })))
244
+ adjacent = true;
245
+ }
246
+ }
247
+ let capacity = 0;
248
+ for (const contribution of left.contributions)
249
+ capacity += contribution.capacity;
250
+ for (const contribution of right.contributions)
251
+ capacity += contribution.capacity;
252
+ return adjacent && Number.isFinite(capacity) ? [left, right] : null;
253
+ }
254
+ function copyRegionCells(cells) {
255
+ return cells.map(cell => ({ col: cell.col, row: cell.row }));
256
+ }
257
+ function pooledRegion(id, kind, tier, parts) {
258
+ const contributions = parts.map(part => ({ id: part.id, capacity: part.capacity, cells: copyRegionCells(part.cells) }));
259
+ return { id, kind, tier, capacity: contributions.reduce((sum, part) => sum + part.capacity, 0),
260
+ cells: contributions.flatMap(part => copyRegionCells(part.cells)), contributions };
261
+ }
262
+ /** Matching kind/tier, disjoint valid source identities/cells, and cardinal contact. Linear in input size.
263
+ * @capability mergeable-footprint-regions check compatible adjacent regions within caller work budgets
264
+ */
265
+ export function canMerge(a, b, budget = {}) {
266
+ return mergeableRegions(a, b, budget) !== null;
267
+ }
268
+ /**
269
+ * Pool compatible footprints under caller-owned `id`, retaining original capacities and identities.
270
+ * Returns null for invalid data, kind/tier mismatch, overlap, no cardinal contact or exhausted budget.
271
+ * Does not mutate inputs or grid reservations; costs, merge width and upgrade policy remain caller-owned.
272
+ * Capacity is the ordered JavaScript-number sum of original contributions, independent of cell area.
273
+ *
274
+ * @capability merge-footprint-regions pool adjacent compatible footprints with serializable capacity provenance
275
+ */
276
+ export function mergeFootprints(a, b, id, budget = {}) {
277
+ if (!validId(id))
278
+ return null;
279
+ const checked = mergeableRegions(a, b, budget);
280
+ if (checked === null)
281
+ return null;
282
+ return pooledRegion(id, a.kind, a.tier, [...checked[0].contributions, ...checked[1].contributions]);
283
+ }
284
+ /**
285
+ * Partition whole original contributions by `leftIds`; both sides must remain cardinally connected.
286
+ * Singletons restore their original id and capacity; larger sides require explicit output identities.
287
+ * Returns null for invalid provenance/budget, unknown or duplicate ids, empty/disconnected sides, or
288
+ * conflicting output identities (including identities from the opposite side's contributions).
289
+ * An output may reuse its own side's source identity. Cell cuts/capacity repricing require caller policy.
290
+ * Both sides inherit the current region kind/tier. Inputs and grid reservations remain unchanged.
291
+ *
292
+ * @capability split-footprint-regions split pooled regions along retained contributions without area-based capacity loss
293
+ */
294
+ export function splitRegion(region, leftIds, ids = {}, budget = {}) {
295
+ if (!withinRegionBudget([region], budget))
296
+ return null;
297
+ const checked = checkedRegion(region);
298
+ if (checked === null || leftIds.length === 0 || leftIds.length >= checked.contributions.length)
299
+ return null;
300
+ const selected = new Set();
301
+ for (const id of leftIds) {
302
+ if (!checked.ids.has(id) || selected.has(id))
303
+ return null;
304
+ selected.add(id);
305
+ }
306
+ const left = [];
307
+ const right = [];
308
+ for (const part of checked.contributions)
309
+ (selected.has(part.id) ? left : right).push(part);
310
+ function side(parts, id) {
311
+ if (parts.length === 1) {
312
+ const part = parts[0];
313
+ if (id !== undefined && id !== part.id)
314
+ return null;
315
+ return { id: part.id, kind: region.kind, tier: region.tier, capacity: part.capacity, cells: copyRegionCells(part.cells) };
316
+ }
317
+ if (id === undefined || !validId(id))
318
+ return null;
319
+ const pooled = pooledRegion(id, region.kind, region.tier, parts);
320
+ const cells = regionCells(pooled.cells);
321
+ return cells !== null && connectedRegion(cells) ? pooled : null;
322
+ }
323
+ const a = side(left, ids.left);
324
+ const b = side(right, ids.right);
325
+ if (a === null || b === null || a.id === b.id)
326
+ return null;
327
+ if (right.some(part => part.id === a.id) || left.some(part => part.id === b.id))
328
+ return null;
329
+ return [a, b];
330
+ }
114
331
  /** Bridges live reservations into `world/placement`'s `PlacementRules.obstacles` so `validatePlacement`/`createPlacementController` see the grid's committed footprints unchanged. */
115
332
  export function footprintObstacles(grid) {
116
333
  return grid.list().map((reservation) => {
@@ -32,4 +32,5 @@ export interface Interiors {
32
32
  toInterior(id: string, exterior: Vec2): Vec2 | null;
33
33
  toExterior(id: string, interior: Vec2): Vec2 | null;
34
34
  }
35
+ /** Frame conversion and bounded movement across an exterior and its interiors; holds config only, no play state. */
35
36
  export declare function createInteriors(config?: InteriorsConfig): Interiors;
@@ -14,12 +14,14 @@ function toExteriorFrame(interior, local) {
14
14
  const rotated = rotate(local, interior.rotation ?? 0);
15
15
  return [rotated[0] + interior.origin[0], rotated[1] + interior.origin[1]];
16
16
  }
17
+ function indexInteriors(interiors) {
18
+ return new Map(interiors.map((interior) => [interior.id, interior]));
19
+ }
20
+ /** Frame conversion and bounded movement across an exterior and its interiors; holds config only, no play state. */
17
21
  export function createInteriors(config = {}) {
18
22
  const radius = config.radius ?? 0;
19
23
  const exterior = config.exterior ?? {};
20
- const byId = new Map();
21
- for (const interior of config.interiors ?? [])
22
- byId.set(interior.id, interior);
24
+ const byId = indexInteriors(config.interiors ?? []);
23
25
  return {
24
26
  move(location, delta) {
25
27
  if (location.space.kind === "exterior") {
@@ -7,11 +7,17 @@ export interface LodSchedulerConfig {
7
7
  beyondInterval?: number | null;
8
8
  stagger?: boolean;
9
9
  }
10
+ /** Plain JSON state of a {@link LodScheduler}: each id's accumulated seconds. */
11
+ export interface LodSchedulerState {
12
+ buckets: Record<string, number>;
13
+ }
10
14
  export interface LodScheduler {
11
15
  bandIndex(distance: number): number;
12
16
  step(id: string, distance: number, dtSeconds: number): number;
13
17
  remove(id: string): void;
14
18
  clear(): void;
15
19
  size(): number;
20
+ snapshot(): LodSchedulerState;
21
+ restore(next: LodSchedulerState): void;
16
22
  }
17
23
  export declare function createLodScheduler(config: LodSchedulerConfig): LodScheduler;
package/dist/world/lod.js CHANGED
@@ -62,5 +62,13 @@ export function createLodScheduler(config) {
62
62
  size() {
63
63
  return buckets.size;
64
64
  },
65
+ snapshot() {
66
+ return { buckets: Object.fromEntries(buckets) };
67
+ },
68
+ restore(next) {
69
+ buckets.clear();
70
+ for (const [id, seconds] of Object.entries(next.buckets))
71
+ buckets.set(id, seconds);
72
+ },
65
73
  };
66
74
  }
@@ -7,7 +7,7 @@
7
7
  * (O(points) per sample), and serializable; it composes onto any height sampler and pairs with
8
8
  * `resolveTerrainField` so the same authored path drives both the rendered mesh and player collision.
9
9
  * @capability path-terrain flatten / grade / carve a heightfield from an authored path with retaining profiles
10
- * @consumer packages/core/src/world/terrain.ts (resolveTerrainField), Games/the-robots roads
10
+ * @consumer packages/core/src/world/terrain.ts (resolveTerrainField), Games/scrap-signal roads
11
11
  */
12
12
  import type { Vec2 } from "./geometry.js";
13
13
  /**
@@ -148,7 +148,7 @@ export interface EditableTerrain extends TerrainField {
148
148
  revertSurfaceDelta(delta: SurfaceDelta): void;
149
149
  /** The current material layer stack (lower index paints under higher). */
150
150
  readonly layers: readonly TerrainMaterialLayer[];
151
- /** Replace the material layer stack; drops blend weights whose layer index no longer exists. */
151
+ /** Replace the material stack, retaining weights by layer id and normalizing when painted layers are removed. */
152
152
  setLayers(layers: readonly TerrainMaterialLayer[]): void;
153
153
  /**
154
154
  * Paint the active `edit.surface` layer's blend weight into the brush footprint, pushing the
@@ -262,5 +262,13 @@ export interface TerraformBrush {
262
262
  config(): Required<Omit<TerraformBrushConfig, "surface">> & {
263
263
  surface: string | undefined;
264
264
  };
265
+ /** The retunable brush settings; the terrain keeps its own `snapshot()`. */
266
+ snapshot(): TerraformBrushState;
267
+ restore(next: TerraformBrushState): void;
268
+ }
269
+ /** Plain JSON state of a {@link TerraformBrush}. */
270
+ export interface TerraformBrushState {
271
+ radius: number;
272
+ strength: number;
265
273
  }
266
274
  export declare function createTerraformBrush(terrain: Pick<EditableTerrain, "apply">, config?: TerraformBrushConfig): TerraformBrush;
@@ -123,14 +123,12 @@ export function createEditableTerrain(config) {
123
123
  const cx = gridX(edit.center[0]);
124
124
  const cz = gridZ(edit.center[1]);
125
125
  let changed = 0;
126
- for (let iz = -rCells; iz <= rCells; iz += 1) {
127
- const gz = Math.round(cz) + iz;
128
- if (gz < 0 || gz >= vertsZ)
129
- continue;
130
- for (let ix = -rCells; ix <= rCells; ix += 1) {
131
- const gx = Math.round(cx) + ix;
132
- if (gx < 0 || gx >= vertsX)
133
- continue;
126
+ const minZ = Math.max(0, Math.round(cz) - rCells);
127
+ const maxZ = Math.min(vertsZ - 1, Math.round(cz) + rCells);
128
+ const minX = Math.max(0, Math.round(cx) - rCells);
129
+ const maxX = Math.min(vertsX - 1, Math.round(cx) + rCells);
130
+ for (let gz = minZ; gz <= maxZ; gz += 1) {
131
+ for (let gx = minX; gx <= maxX; gx += 1) {
134
132
  const wx = vertexWorldX(gx);
135
133
  const wz = vertexWorldZ(gz);
136
134
  const dx = wx - edit.center[0];
@@ -230,14 +228,12 @@ export function createEditableTerrain(config) {
230
228
  const rCells = Math.ceil(edit.radius / cellSize) + 1;
231
229
  const cx = gridX(edit.center[0]);
232
230
  const cz = gridZ(edit.center[1]);
233
- for (let iz = -rCells; iz <= rCells; iz += 1) {
234
- const gz = Math.floor(cz) + iz;
235
- if (gz < 0 || gz >= rows)
236
- continue;
237
- for (let ix = -rCells; ix <= rCells; ix += 1) {
238
- const gx = Math.floor(cx) + ix;
239
- if (gx < 0 || gx >= cols)
240
- continue;
231
+ const minZ = Math.max(0, Math.floor(cz) - rCells);
232
+ const maxZ = Math.min(rows - 1, Math.floor(cz) + rCells);
233
+ const minX = Math.max(0, Math.floor(cx) - rCells);
234
+ const maxX = Math.min(cols - 1, Math.floor(cx) + rCells);
235
+ for (let gz = minZ; gz <= maxZ; gz += 1) {
236
+ for (let gx = minX; gx <= maxX; gx += 1) {
241
237
  const wx = cellWorldX(gx);
242
238
  const wz = cellWorldZ(gz);
243
239
  const dx = wx - edit.center[0];
@@ -469,9 +465,41 @@ export function createEditableTerrain(config) {
469
465
  return layers;
470
466
  },
471
467
  setLayers(next) {
468
+ const previous = layers;
469
+ const previousWeights = weights;
472
470
  layers = next.map((layer) => ({ ...layer }));
473
- // Layer count changed shape: drop the weight buffer, blends re-seed from surfaces on demand.
474
- weights = null;
471
+ if (previousWeights === null || layers.length === 0) {
472
+ weights = null;
473
+ return;
474
+ }
475
+ const previousIndices = layers.map((layer) => previous.findIndex((entry) => entry.id === layer.id));
476
+ const buffer = new Float32Array(cellCount * layers.length);
477
+ let retained = false;
478
+ for (let cell = 0; cell < cellCount; cell += 1) {
479
+ let total = 0;
480
+ for (let layer = 0; layer < layers.length; layer += 1) {
481
+ const before = previousIndices[layer];
482
+ const weight = before < 0 ? 0 : previousWeights[cell * previous.length + before];
483
+ buffer[cell * layers.length + layer] = weight;
484
+ total += weight;
485
+ }
486
+ if (total > 0) {
487
+ retained = true;
488
+ let previousTotal = 0;
489
+ for (let layer = 0; layer < previous.length; layer += 1)
490
+ previousTotal += previousWeights[cell * previous.length + layer];
491
+ if (total < previousTotal) {
492
+ for (let layer = 0; layer < layers.length; layer += 1)
493
+ buffer[cell * layers.length + layer] /= total;
494
+ }
495
+ }
496
+ else {
497
+ const fallback = layers.findIndex((layer) => layer.surface === surfaces[cell]);
498
+ if (fallback >= 0)
499
+ buffer[cell * layers.length + fallback] = 1;
500
+ }
501
+ }
502
+ weights = retained ? buffer : null;
475
503
  },
476
504
  blendRecording(edit, record) {
477
505
  return blendPaint(edit, record);
@@ -787,5 +815,12 @@ export function createTerraformBrush(terrain, config = {}) {
787
815
  config() {
788
816
  return { radius, strength, falloff, surface };
789
817
  },
818
+ snapshot() {
819
+ return { radius, strength };
820
+ },
821
+ restore(next) {
822
+ radius = next.radius;
823
+ strength = next.strength;
824
+ },
790
825
  };
791
826
  }