@jgengine/core 0.18.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/ai/decisionGraph.d.ts +50 -4
  3. package/dist/ai/decisionGraph.js +159 -23
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/ai/groupAssist.d.ts +10 -0
  7. package/dist/ai/groupAssist.js +11 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/threat.d.ts +8 -0
  11. package/dist/ai/threat.js +10 -0
  12. package/dist/anim/animGraph.d.ts +43 -1
  13. package/dist/anim/animGraph.js +326 -10
  14. package/dist/anim/boneTexture.d.ts +55 -0
  15. package/dist/anim/boneTexture.js +43 -0
  16. package/dist/anim/footPlacement.d.ts +57 -0
  17. package/dist/anim/footPlacement.js +67 -0
  18. package/dist/anim/locomotionGraph.d.ts +18 -1
  19. package/dist/anim/locomotionGraph.js +42 -3
  20. package/dist/audio/audioFalloff.d.ts +6 -0
  21. package/dist/audio/doppler.d.ts +21 -0
  22. package/dist/audio/doppler.js +32 -0
  23. package/dist/audio/engineLayers.d.ts +83 -0
  24. package/dist/audio/engineLayers.js +115 -0
  25. package/dist/audio/voiceAllocator.d.ts +46 -0
  26. package/dist/audio/voiceAllocator.js +76 -0
  27. package/dist/combat/abilityKit.d.ts +20 -0
  28. package/dist/combat/abilityKit.js +41 -4
  29. package/dist/combat/death.d.ts +11 -3
  30. package/dist/combat/death.js +3 -3
  31. package/dist/combat/magazine.d.ts +9 -0
  32. package/dist/combat/magazine.js +24 -2
  33. package/dist/combat/projectiles.d.ts +94 -2
  34. package/dist/combat/projectiles.js +282 -61
  35. package/dist/combat/regenShield.d.ts +14 -0
  36. package/dist/combat/regenShield.js +23 -7
  37. package/dist/combat/shotOrigin.d.ts +6 -0
  38. package/dist/combat/shotOrigin.js +6 -0
  39. package/dist/combat/weaponFeedback.d.ts +36 -0
  40. package/dist/combat/weaponFeedback.js +42 -0
  41. package/dist/combat/weaponHandling.d.ts +161 -0
  42. package/dist/combat/weaponHandling.js +202 -0
  43. package/dist/combat/weaponPresentation.d.ts +115 -0
  44. package/dist/combat/weaponPresentation.js +96 -0
  45. package/dist/crafting/recipeCatalog.d.ts +16 -0
  46. package/dist/crafting/recipeCatalog.js +55 -0
  47. package/dist/devtools/warnOnce.d.ts +8 -0
  48. package/dist/devtools/warnOnce.js +18 -0
  49. package/dist/economy/auctionBook.d.ts +1 -1
  50. package/dist/economy/auctionBook.js +11 -1
  51. package/dist/economy/listingBook.d.ts +1 -1
  52. package/dist/economy/listingBook.js +11 -1
  53. package/dist/economy/resourceLedger.js +3 -3
  54. package/dist/economy/shopStock.js +7 -6
  55. package/dist/economy/techTree.js +7 -6
  56. package/dist/economy/unlockPoints.js +3 -6
  57. package/dist/editor/commandApply.js +89 -16
  58. package/dist/editor/commands.d.ts +38 -1
  59. package/dist/editor/commands.js +320 -3
  60. package/dist/editor/creatorStorage.d.ts +59 -0
  61. package/dist/editor/creatorStorage.js +164 -0
  62. package/dist/editor/document.d.ts +4 -0
  63. package/dist/editor/document.js +119 -18
  64. package/dist/editor/environment.js +8 -0
  65. package/dist/editor/index.d.ts +2 -0
  66. package/dist/editor/liveSync.d.ts +4 -1
  67. package/dist/editor/liveSync.js +26 -10
  68. package/dist/editor/materialAuthoring.d.ts +14 -0
  69. package/dist/editor/materialAuthoring.js +38 -0
  70. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  71. package/dist/editor/modelNodeVisibility.js +29 -0
  72. package/dist/editor/movementCatalog.d.ts +90 -0
  73. package/dist/editor/movementCatalog.js +144 -0
  74. package/dist/editor/simulation.d.ts +60 -0
  75. package/dist/editor/simulation.js +202 -0
  76. package/dist/editor/staticPrefab.d.ts +17 -0
  77. package/dist/editor/staticPrefab.js +51 -0
  78. package/dist/editor/types.d.ts +15 -0
  79. package/dist/faction/reputation.d.ts +6 -0
  80. package/dist/faction/reputation.js +11 -0
  81. package/dist/game/cameraConfig.d.ts +54 -2
  82. package/dist/game/clipRoles.d.ts +2 -1
  83. package/dist/game/clipRoles.js +7 -3
  84. package/dist/game/contentValidation.d.ts +69 -0
  85. package/dist/game/contentValidation.js +118 -0
  86. package/dist/game/controlGate.d.ts +8 -0
  87. package/dist/game/controlGate.js +43 -6
  88. package/dist/game/defineGame.d.ts +32 -3
  89. package/dist/game/dialogueGraph.d.ts +20 -0
  90. package/dist/game/dialogueGraph.js +107 -0
  91. package/dist/game/events.d.ts +11 -0
  92. package/dist/game/keyValueStore.d.ts +17 -4
  93. package/dist/game/keyValueStore.js +35 -23
  94. package/dist/game/lootModifiers.d.ts +17 -0
  95. package/dist/game/lootModifiers.js +26 -0
  96. package/dist/game/lootTable.d.ts +4 -0
  97. package/dist/game/lootTable.js +16 -0
  98. package/dist/game/narrativeAuthoring.d.ts +67 -0
  99. package/dist/game/narrativeAuthoring.js +122 -0
  100. package/dist/game/playableGame.d.ts +134 -35
  101. package/dist/game/quest.d.ts +15 -0
  102. package/dist/game/quest.js +209 -63
  103. package/dist/game/questCatalog.d.ts +50 -0
  104. package/dist/game/questCatalog.js +205 -0
  105. package/dist/game/saveStore.d.ts +6 -5
  106. package/dist/game/saveStore.js +57 -57
  107. package/dist/game/systemRuntime.d.ts +2 -2
  108. package/dist/game/systemRuntime.js +84 -33
  109. package/dist/game/toasts.d.ts +8 -0
  110. package/dist/game/toasts.js +7 -0
  111. package/dist/game/vfxInstance.d.ts +9 -0
  112. package/dist/game/vfxInstance.js +12 -1
  113. package/dist/game/viewports.d.ts +43 -0
  114. package/dist/game/viewports.js +46 -0
  115. package/dist/game/worldItem.d.ts +5 -1
  116. package/dist/game/worldItem.js +0 -1
  117. package/dist/gameplay.d.ts +3 -0
  118. package/dist/gameplay.js +3 -0
  119. package/dist/input/actionBindings.d.ts +15 -1
  120. package/dist/input/actionBindings.js +14 -3
  121. package/dist/input/actionContexts.d.ts +25 -1
  122. package/dist/input/actionContexts.js +82 -25
  123. package/dist/input/axisInput.d.ts +4 -0
  124. package/dist/input/axisInput.js +4 -0
  125. package/dist/input/axisShaper.d.ts +73 -0
  126. package/dist/input/axisShaper.js +105 -0
  127. package/dist/input/gamepadModel.d.ts +36 -2
  128. package/dist/input/gamepadModel.js +69 -30
  129. package/dist/input/haptics.d.ts +50 -0
  130. package/dist/input/haptics.js +74 -0
  131. package/dist/input/inputBuffer.d.ts +40 -0
  132. package/dist/input/inputBuffer.js +59 -0
  133. package/dist/input/lookChannel.d.ts +10 -0
  134. package/dist/input/lookChannel.js +6 -0
  135. package/dist/input/pointer.d.ts +2 -0
  136. package/dist/input/pointer.js +4 -0
  137. package/dist/interaction/promptRegistry.d.ts +5 -4
  138. package/dist/interaction/promptRegistry.js +8 -2
  139. package/dist/inventory/inventoryModel.js +9 -6
  140. package/dist/inventory/shapedGrid.d.ts +48 -9
  141. package/dist/inventory/shapedGrid.js +69 -10
  142. package/dist/item/affix.d.ts +6 -0
  143. package/dist/item/affix.js +6 -0
  144. package/dist/item/durability.js +4 -0
  145. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  146. package/dist/item/itemInstanceRegistry.js +111 -7
  147. package/dist/item/itemgen.d.ts +1 -1
  148. package/dist/item/itemgen.js +1 -1
  149. package/dist/item/modularItem.js +7 -1
  150. package/dist/material/appearanceSignals.d.ts +53 -0
  151. package/dist/material/appearanceSignals.js +47 -0
  152. package/dist/material/hairCards.d.ts +47 -0
  153. package/dist/material/hairCards.js +136 -0
  154. package/dist/material/materialAsset.d.ts +162 -0
  155. package/dist/material/materialAsset.js +382 -0
  156. package/dist/meta/changelog.d.ts +1 -1
  157. package/dist/meta/changelog.js +444 -1
  158. package/dist/movement/characterController.d.ts +5 -2
  159. package/dist/movement/characterController.js +50 -23
  160. package/dist/movement/freeFlight.d.ts +2 -2
  161. package/dist/movement/freeFlight.js +3 -3
  162. package/dist/movement/glideModel.d.ts +3 -0
  163. package/dist/movement/glideModel.js +6 -0
  164. package/dist/movement/grappleSwing.d.ts +2 -0
  165. package/dist/movement/grappleSwing.js +5 -1
  166. package/dist/movement/movementModel.d.ts +63 -1
  167. package/dist/movement/movementModel.js +136 -26
  168. package/dist/movement/movementProbe.d.ts +51 -0
  169. package/dist/movement/movementProbe.js +119 -0
  170. package/dist/movement/playerMovement.d.ts +48 -2
  171. package/dist/movement/playerMovement.js +493 -87
  172. package/dist/movement/poseState.d.ts +2 -0
  173. package/dist/movement/poseState.js +29 -1
  174. package/dist/movement/solidObstacles.d.ts +11 -1
  175. package/dist/movement/solidObstacles.js +48 -3
  176. package/dist/movement/terrainGrade.d.ts +13 -0
  177. package/dist/movement/terrainGrade.js +33 -0
  178. package/dist/movement/voxelController.d.ts +2 -2
  179. package/dist/movement/voxelController.js +10 -10
  180. package/dist/nav/navFromEnvironment.d.ts +9 -3
  181. package/dist/nav/navFromEnvironment.js +18 -12
  182. package/dist/nav/navGrid.d.ts +19 -1
  183. package/dist/nav/navGrid.js +26 -10
  184. package/dist/nav/navMesh.d.ts +43 -2
  185. package/dist/nav/navMesh.js +534 -128
  186. package/dist/nav/solidRoute.d.ts +50 -0
  187. package/dist/nav/solidRoute.js +144 -0
  188. package/dist/physics/aircraftDynamics.d.ts +390 -0
  189. package/dist/physics/aircraftDynamics.js +704 -0
  190. package/dist/physics/ballisticSweep.d.ts +10 -3
  191. package/dist/physics/ballisticSweep.js +53 -29
  192. package/dist/physics/boatDynamics.d.ts +116 -0
  193. package/dist/physics/boatDynamics.js +160 -0
  194. package/dist/physics/drivableVehicle.d.ts +22 -8
  195. package/dist/physics/drivableVehicle.js +4 -2
  196. package/dist/physics/flightDynamics.d.ts +21 -4
  197. package/dist/physics/flightDynamics.js +28 -5
  198. package/dist/physics/forceVolume.d.ts +41 -0
  199. package/dist/physics/forceVolume.js +88 -0
  200. package/dist/physics/handlingProbe.d.ts +249 -0
  201. package/dist/physics/handlingProbe.js +606 -0
  202. package/dist/physics/kinematicVehicle.d.ts +40 -5
  203. package/dist/physics/kinematicVehicle.js +17 -2
  204. package/dist/physics/physicsBackend.d.ts +12 -0
  205. package/dist/physics/physicsBackendConformance.js +34 -0
  206. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  207. package/dist/physics/physicsWorldBackend.js +52 -3
  208. package/dist/physics/spatialGrid.d.ts +1 -1
  209. package/dist/physics/spatialGrid.js +8 -9
  210. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  211. package/dist/physics/vehicleBackendLink.js +102 -0
  212. package/dist/physics/vehicleDynamics.d.ts +375 -0
  213. package/dist/physics/vehicleDynamics.js +737 -0
  214. package/dist/physics/vehicleObstacles.d.ts +3 -2
  215. package/dist/physics/vehicleObstacles.js +29 -5
  216. package/dist/physics/worldColliders.d.ts +1 -1
  217. package/dist/physics/worldColliders.js +35 -3
  218. package/dist/puzzle/cellGrid.d.ts +15 -9
  219. package/dist/puzzle/cellGrid.js +13 -9
  220. package/dist/render/lookPreset.d.ts +9 -2
  221. package/dist/render/lookPreset.js +29 -2
  222. package/dist/render/postProcessing.d.ts +30 -0
  223. package/dist/render/postProcessing.js +1 -1
  224. package/dist/runtime/cameraDirector.d.ts +54 -3
  225. package/dist/runtime/cameraDirector.js +187 -0
  226. package/dist/runtime/commandDispatch.d.ts +14 -0
  227. package/dist/runtime/commandDispatch.js +19 -0
  228. package/dist/runtime/commandInput.d.ts +37 -0
  229. package/dist/runtime/commandInput.js +58 -0
  230. package/dist/runtime/commandRunner.d.ts +27 -0
  231. package/dist/runtime/commandRunner.js +31 -3
  232. package/dist/runtime/context/combat.d.ts +12 -4
  233. package/dist/runtime/context/combat.js +179 -11
  234. package/dist/runtime/context/deathLoot.d.ts +11 -8
  235. package/dist/runtime/context/deathLoot.js +17 -16
  236. package/dist/runtime/context/scene.d.ts +2 -0
  237. package/dist/runtime/context/scene.js +13 -21
  238. package/dist/runtime/descriptors/baseline.js +34 -3
  239. package/dist/runtime/descriptors/features.d.ts +2 -1
  240. package/dist/runtime/descriptors/features.js +18 -0
  241. package/dist/runtime/gameContext.js +86 -13
  242. package/dist/runtime/gameContextTypes.d.ts +33 -3
  243. package/dist/runtime/gameRuntime.d.ts +3 -1
  244. package/dist/runtime/gameRuntime.js +4 -0
  245. package/dist/runtime/headlessRunner.d.ts +3 -1
  246. package/dist/runtime/headlessRunner.js +11 -11
  247. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  248. package/dist/runtime/hostedGameRunner.js +156 -10
  249. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  250. package/dist/runtime/hostedWorldSession.js +154 -16
  251. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  252. package/dist/runtime/inputRecorder.d.ts +1 -1
  253. package/dist/runtime/inputRecorder.js +21 -1
  254. package/dist/runtime/inputSnapshot.d.ts +22 -4
  255. package/dist/runtime/inputSnapshot.js +41 -3
  256. package/dist/runtime/localPlayers.d.ts +73 -0
  257. package/dist/runtime/localPlayers.js +119 -0
  258. package/dist/runtime/runtimeSave.d.ts +10 -6
  259. package/dist/runtime/runtimeSave.js +11 -5
  260. package/dist/runtime/simContext.d.ts +8 -3
  261. package/dist/runtime/simContext.js +7 -3
  262. package/dist/runtime/simSnapshot.js +2 -0
  263. package/dist/runtime/transport.d.ts +9 -0
  264. package/dist/runtime/transport.js +7 -1
  265. package/dist/runtime/worldReplication.d.ts +2 -0
  266. package/dist/runtime/worldReplication.js +1 -1
  267. package/dist/scene/assetCatalog.d.ts +2 -0
  268. package/dist/scene/behaviorRuntime.d.ts +38 -7
  269. package/dist/scene/behaviorRuntime.js +293 -30
  270. package/dist/scene/behaviors.d.ts +35 -2
  271. package/dist/scene/behaviors.js +29 -0
  272. package/dist/scene/entityStore.d.ts +14 -3
  273. package/dist/scene/entityStore.js +45 -9
  274. package/dist/scene/mount.d.ts +9 -0
  275. package/dist/scene/mount.js +35 -0
  276. package/dist/scene/possession.d.ts +2 -0
  277. package/dist/scene/possession.js +21 -1
  278. package/dist/scene/sceneKinds.d.ts +9 -0
  279. package/dist/scene/sceneRaycast.d.ts +7 -0
  280. package/dist/scene/sceneRaycast.js +55 -0
  281. package/dist/scene/spatial.d.ts +20 -1
  282. package/dist/scene/spatial.js +94 -2
  283. package/dist/scene/vehicleSeat.d.ts +41 -3
  284. package/dist/scene/vehicleSeat.js +94 -3
  285. package/dist/sensor/concealment.d.ts +6 -0
  286. package/dist/sensor/concealment.js +8 -0
  287. package/dist/sensor/freezeMonitor.d.ts +6 -0
  288. package/dist/sensor/freezeMonitor.js +8 -0
  289. package/dist/sensor/perception.d.ts +11 -1
  290. package/dist/sensor/perception.js +57 -10
  291. package/dist/sensor/recordingBuffer.d.ts +3 -0
  292. package/dist/sensor/recordingBuffer.js +8 -0
  293. package/dist/session/roundState.d.ts +4 -0
  294. package/dist/session/roundState.js +11 -0
  295. package/dist/settings/frameRateLimit.d.ts +16 -0
  296. package/dist/settings/frameRateLimit.js +37 -0
  297. package/dist/settings/graphicsProfile.d.ts +2 -0
  298. package/dist/settings/graphicsProfile.js +12 -0
  299. package/dist/settings/graphicsSettings.d.ts +40 -0
  300. package/dist/settings/graphicsSettings.js +81 -0
  301. package/dist/settings/settingsModel.d.ts +9 -1
  302. package/dist/settings/settingsModel.js +10 -0
  303. package/dist/stats/accumulatorMeter.d.ts +10 -0
  304. package/dist/stats/accumulatorMeter.js +6 -0
  305. package/dist/stats/eventMeter.d.ts +10 -3
  306. package/dist/stats/eventMeter.js +6 -3
  307. package/dist/store/defineStore.d.ts +3 -1
  308. package/dist/store/defineStore.js +3 -1
  309. package/dist/store/observableKeyedStore.d.ts +1 -1
  310. package/dist/store/observableKeyedStore.js +8 -6
  311. package/dist/time/simClock.d.ts +6 -2
  312. package/dist/time/simClock.js +5 -0
  313. package/dist/vfx/feedbackMixer.d.ts +61 -0
  314. package/dist/vfx/feedbackMixer.js +106 -0
  315. package/dist/vfx/particleDirector.d.ts +58 -6
  316. package/dist/vfx/particleDirector.js +113 -22
  317. package/dist/vfx/particles.d.ts +68 -5
  318. package/dist/vfx/particles.js +284 -32
  319. package/dist/visibility/assetStreaming.d.ts +21 -3
  320. package/dist/visibility/assetStreaming.js +108 -24
  321. package/dist/visibility/settings.d.ts +8 -2
  322. package/dist/visibility/settings.js +3 -0
  323. package/dist/visibility/simulationCulling.d.ts +8 -1
  324. package/dist/visibility/simulationCulling.js +7 -0
  325. package/dist/visibility/spatialIndex.d.ts +21 -0
  326. package/dist/visibility/spatialIndex.js +16 -0
  327. package/dist/visibility/visibilitySystem.js +1 -0
  328. package/dist/work/expedition.d.ts +110 -0
  329. package/dist/work/expedition.js +195 -0
  330. package/dist/work/staffedStation.d.ts +29 -0
  331. package/dist/work/staffedStation.js +27 -0
  332. package/dist/world/authoredAnimation.d.ts +38 -0
  333. package/dist/world/authoredAnimation.js +99 -0
  334. package/dist/world/authoredObjects.d.ts +24 -4
  335. package/dist/world/authoredObjects.js +43 -16
  336. package/dist/world/authoredSimulation.d.ts +74 -0
  337. package/dist/world/authoredSimulation.js +302 -0
  338. package/dist/world/authoredSolids.d.ts +46 -0
  339. package/dist/world/authoredSolids.js +75 -0
  340. package/dist/world/authoredWeather.d.ts +120 -0
  341. package/dist/world/authoredWeather.js +314 -0
  342. package/dist/world/buildingGenerator.js +2 -2
  343. package/dist/world/buildingKit.d.ts +11 -4
  344. package/dist/world/buildingKit.js +21 -8
  345. package/dist/world/buildings.d.ts +34 -2
  346. package/dist/world/buildings.js +11 -1
  347. package/dist/world/cityKind.d.ts +7 -0
  348. package/dist/world/cityKind.js +52 -1
  349. package/dist/world/environmentForces.d.ts +21 -0
  350. package/dist/world/environmentForces.js +42 -0
  351. package/dist/world/features.d.ts +32 -5
  352. package/dist/world/features.js +6 -0
  353. package/dist/world/footprintGrid.d.ts +62 -0
  354. package/dist/world/footprintGrid.js +217 -0
  355. package/dist/world/interiors.d.ts +1 -0
  356. package/dist/world/interiors.js +5 -3
  357. package/dist/world/lod.d.ts +6 -0
  358. package/dist/world/lod.js +8 -0
  359. package/dist/world/pathTerrain.d.ts +1 -1
  360. package/dist/world/terraform.d.ts +9 -1
  361. package/dist/world/terraform.js +53 -18
  362. package/dist/world/walls.d.ts +7 -0
  363. package/dist/world/walls.js +7 -0
  364. package/dist/world/weather.d.ts +17 -0
  365. package/dist/world/weather.js +116 -20
  366. package/dist/world/worldSolids.d.ts +95 -0
  367. package/dist/world/worldSolids.js +351 -0
  368. package/dist/world.d.ts +6 -6
  369. package/dist/world.js +5 -5
  370. package/package.json +1 -1
  371. package/skills/ce-handoff/SKILL.md +2 -2
  372. package/skills/game-design/SKILL.md +2 -2
  373. package/skills/jgengine/SKILL.md +28 -6
  374. package/skills/jgengine/api.md +139 -99
  375. package/skills/jgengine/capabilities.md +28 -0
  376. package/skills/jgengine/recipes/minimal-game.md +10 -2
  377. package/skills/jgengine-combat/SKILL.md +13 -2
  378. package/skills/jgengine-combat/api.md +150 -103
  379. package/skills/jgengine-combat/capabilities.md +29 -1
  380. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  381. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  382. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  383. package/skills/jgengine-combat/reference.md +24 -2
  384. package/skills/jgengine-gameplay/SKILL.md +63 -0
  385. package/skills/jgengine-gameplay/api.md +583 -463
  386. package/skills/jgengine-gameplay/capabilities.md +92 -0
  387. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  388. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  389. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  390. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  391. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  392. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  393. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  394. package/skills/jgengine-gameplay/reference-systems.md +123 -0
  395. package/skills/jgengine-world/SKILL.md +25 -4
  396. package/skills/jgengine-world/api.md +1045 -655
  397. package/skills/jgengine-world/capabilities.md +246 -9
  398. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  399. package/skills/jgengine-world/recipes/controls.md +48 -0
  400. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  401. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  402. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  403. package/skills/jgengine-world/reference.md +180 -4
  404. package/skills/jgengine-world/visibility.md +139 -0
@@ -1,11 +1,15 @@
1
+ import { createInputBuffer } from "../input/inputBuffer.js";
1
2
  import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
2
- import { createCharacterController } from "./characterController.js";
3
+ import { createCharacterController, } from "./characterController.js";
3
4
  import { groundFieldFor, hasEnvironmentTerrain, sampleSlope } from "../world/terrain.js";
4
- import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, MOVEMENT_TUNING, } from "./movementModel.js";
5
+ import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, DEFAULT_OBSTACLE_PLAYER_HEIGHT, motionStepSeconds, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, resolveObstacleVerticalStep, snapPositionToGrid, } from "./movementModel.js";
5
6
  import { advanceFreeFlight, createFreeFlightState, resolveFlightStep, resolveFreeFlightIntentFromInput, } from "./freeFlight.js";
6
7
  import { solidObstaclesNear } from "./solidObstacles.js";
8
+ import { withPoseRollback } from "./poseState.js";
7
9
  import { approachYaw, steerYaw } from "./steering.js";
10
+ import { resolveTerrainGradeStep } from "./terrainGrade.js";
8
11
  import { advanceVoxelPlayer, createVoxelPlayerBody, } from "./voxelController.js";
12
+ const CONTROLLER_CONFIG_FIELDS = ["radius", "height", "stepHeight", "maxSlopeDeg", "skinWidth", "crouchHeight", "snapDistance", "maxSlides", "mask"];
9
13
  const DEFAULT_TURN_SPEED = 2.4;
10
14
  const DEFAULT_WALK_SPEED = 2;
11
15
  const DEFAULT_SWIM_SPEED_MULTIPLIER = 0.65;
@@ -34,25 +38,41 @@ export function resolvePhysicsTuning(physics) {
34
38
  }
35
39
  /** Gather a game's collision/movement/physics/world config into a {@link PlayerMovementTuning} — call once per world; both the shell and a host pass the result to {@link stepPlayerMovement}. */
36
40
  export function resolvePlayerMovementTuning(opts) {
37
- const physics = resolvePhysicsTuning(opts.physics);
38
- const backpedal = opts.movement?.backpedalMult;
39
- const overrides = backpedal === undefined ? physics : { ...(physics ?? {}), backpedalSpeedMultiplier: backpedal };
41
+ const overrides = {
42
+ get gravityAcceleration() { return opts.physics?.gravity === undefined ? undefined : -opts.physics.gravity; },
43
+ get jumpVelocity() { return opts.physics?.jumpVelocity; },
44
+ get backpedalSpeedMultiplier() { return opts.movement?.backpedalMult; },
45
+ get groundAcceleration() { return opts.movement?.feel?.groundAcceleration; },
46
+ get airAcceleration() { return opts.movement?.feel?.airAcceleration; },
47
+ get groundFriction() { return opts.movement?.feel?.groundFriction; },
48
+ get runSpeedMultiplier() { return opts.movement?.feel?.runMultiplier; },
49
+ get crouchSpeedMultiplier() { return opts.movement?.feel?.crouchMultiplier; },
50
+ get jumpBufferMs() { return opts.movement?.feel?.jumpBufferMs; },
51
+ get coyoteMs() { return opts.movement?.feel?.coyoteMs; },
52
+ get jumpCutFactor() { return opts.movement?.feel?.jumpCutFactor; },
53
+ get apexGravityScale() { return opts.movement?.feel?.apexGravityScale; },
54
+ get apexSpeed() { return opts.movement?.feel?.apexSpeed; },
55
+ get fallGravityScale() { return opts.movement?.feel?.fallGravityScale; },
56
+ get landingRecoveryMs() { return opts.movement?.feel?.landingRecoveryMs; },
57
+ get landingSpeedScale() { return opts.movement?.feel?.landingSpeedScale; },
58
+ };
59
+ const defaultCapsule = {
60
+ radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
61
+ height: 1.8,
62
+ stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
63
+ };
64
+ const controller = {
65
+ get backend() { return opts.physics.backend; },
66
+ get capsule() { return opts.physics?.controller ?? defaultCapsule; },
67
+ };
40
68
  return {
41
- ...(opts.collision === undefined ? {} : { collision: opts.collision }),
42
- ...(opts.movement === undefined ? {} : { movement: opts.movement }),
43
- ...(overrides === undefined ? {} : { physics: overrides }),
44
- ...(opts.physics?.backend === undefined
45
- ? {}
46
- : {
47
- controller: {
48
- backend: opts.physics.backend,
49
- capsule: opts.physics.controller ?? {
50
- radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
51
- height: 1.8,
52
- stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
53
- },
54
- },
55
- }),
69
+ get collision() { return opts.collision; },
70
+ get movement() { return opts.movement; },
71
+ get physics() {
72
+ return opts.physics === undefined && opts.movement?.feel === undefined && opts.movement?.backpedalMult === undefined
73
+ ? undefined : overrides;
74
+ },
75
+ get controller() { return opts.physics?.backend === undefined ? undefined : controller; },
56
76
  ground: groundFieldFor(opts.world),
57
77
  hasTerrain: hasEnvironmentTerrain(opts.world),
58
78
  };
@@ -66,21 +86,190 @@ function storeFor(ctx) {
66
86
  solids: { count: -1, set: new Set() },
67
87
  };
68
88
  stores.set(ctx, store);
89
+ const retained = store;
90
+ ctx.scene.entity.subscribeMembership(() => {
91
+ for (const [userId, state] of retained.players) {
92
+ if (state.liveEntityId !== null) {
93
+ if (ctx.scene.entity.get(state.liveEntityId) === null)
94
+ retained.players.delete(userId);
95
+ }
96
+ else {
97
+ const entityId = ctx.player.possession.active(userId);
98
+ if (ctx.scene.entity.get(entityId) !== null)
99
+ state.liveEntityId = entityId;
100
+ }
101
+ }
102
+ });
69
103
  }
70
104
  return store;
71
105
  }
72
- function stateFor(store, userId) {
106
+ function stateFor(ctx, store, userId) {
73
107
  let state = store.players.get(userId);
74
108
  if (state === undefined) {
75
- state = { heading: 0, facing: null, voxelBody: null, motion: null, flight: null, controller: null, controllerJumpHeld: false };
109
+ state = {
110
+ heading: 0,
111
+ facing: null,
112
+ voxelBody: null,
113
+ motion: null,
114
+ flight: null,
115
+ controller: null,
116
+ controllerConfig: null,
117
+ controllerJumpHeld: false,
118
+ jumpBuffer: null,
119
+ pendingController: null,
120
+ heightfieldHeight: null,
121
+ heightfieldEntityId: null,
122
+ liveEntityId: null,
123
+ entityId: null,
124
+ telemetry: null,
125
+ };
126
+ const entityId = ctx.player.possession.active(userId);
127
+ if (ctx.scene.entity.get(entityId) !== null)
128
+ state.liveEntityId = entityId;
76
129
  store.players.set(userId, state);
77
130
  }
78
131
  return state;
79
132
  }
133
+ function invalidateTelemetry(state) {
134
+ state.entityId = null;
135
+ state.telemetry = null;
136
+ }
137
+ function validateCollisionHeight(height) {
138
+ if (!Number.isFinite(height) || height <= 0) {
139
+ throw new RangeError("PlayerMovementConfig.collisionHeight must be finite and positive.");
140
+ }
141
+ }
142
+ function retuneController(state, config) {
143
+ const previous = state.controllerConfig;
144
+ if (previous !== null) {
145
+ let changed = false;
146
+ for (const field of CONTROLLER_CONFIG_FIELDS) {
147
+ if (previous[field] !== config[field]) {
148
+ changed = true;
149
+ break;
150
+ }
151
+ }
152
+ if (!changed)
153
+ return;
154
+ }
155
+ const next = {
156
+ radius: config.radius,
157
+ height: config.height,
158
+ stepHeight: config.stepHeight,
159
+ maxSlopeDeg: config.maxSlopeDeg,
160
+ skinWidth: config.skinWidth,
161
+ crouchHeight: config.crouchHeight,
162
+ snapDistance: config.snapDistance,
163
+ maxSlides: config.maxSlides,
164
+ mask: config.mask,
165
+ };
166
+ state.controller.retune(next);
167
+ state.controllerConfig = next;
168
+ }
169
+ function positionAccepted(ctx, entityId, x, y, z) {
170
+ const actual = ctx.scene.entity.get(entityId)?.position;
171
+ return actual !== undefined && actual[0] === x && actual[1] === y && actual[2] === z;
172
+ }
173
+ function updateTelemetry(state, entityId, grounded, verticalVelocity, crouching, collisionHeight, requestedCollisionHeight, collisionHeightBlocked) {
174
+ state.entityId = entityId;
175
+ const telemetry = state.telemetry ??= { grounded, verticalVelocity, crouching };
176
+ telemetry.grounded = grounded;
177
+ telemetry.verticalVelocity = verticalVelocity;
178
+ telemetry.crouching = crouching;
179
+ if (collisionHeight === undefined) {
180
+ delete telemetry.collisionHeight;
181
+ delete telemetry.requestedCollisionHeight;
182
+ delete telemetry.collisionHeightBlocked;
183
+ }
184
+ else {
185
+ telemetry.collisionHeight = collisionHeight;
186
+ telemetry.requestedCollisionHeight = requestedCollisionHeight;
187
+ telemetry.collisionHeightBlocked = collisionHeightBlocked;
188
+ }
189
+ }
190
+ /**
191
+ * Read the entity's last shared movement result through indexed possession ownership. Returns `null` before a
192
+ * step, after movement restore/forget, when a commit policy replaces the motor proposal, or for an entity not
193
+ * currently driven by this motor. Reuses one live view;
194
+ * custom movers supply their own animation parameters. Flight never reports ground contact or a walking crouch.
195
+ *
196
+ * @capability movement-telemetry read physical grounded, vertical velocity and crouch state without animation-store writes
197
+ */
198
+ export function playerMovementTelemetry(ctx, entityId) {
199
+ const userId = ctx.player.possession.ownerOf(entityId) ?? entityId;
200
+ if (ctx.player.possession.active(userId) !== entityId || ctx.scene.entity.get(entityId) === null)
201
+ return null;
202
+ const state = stores.get(ctx)?.players.get(userId);
203
+ return state?.entityId === entityId ? state.telemetry : null;
204
+ }
80
205
  /** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
81
206
  export function playerMovementHeading(ctx, userId) {
82
207
  return stores.get(ctx)?.players.get(userId)?.heading ?? 0;
83
208
  }
209
+ function copyController(state) {
210
+ return { ...state, position: [...state.position], groundNormal: [...state.groundNormal] };
211
+ }
212
+ /** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
213
+ export function snapshotPlayerMovement(ctx, userId) {
214
+ const state = stores.get(ctx)?.players.get(userId);
215
+ if (state === undefined)
216
+ return null;
217
+ const controller = state.controller?.snapshot() ?? state.pendingController;
218
+ return {
219
+ heading: state.heading,
220
+ facing: state.facing,
221
+ voxelBody: state.voxelBody === null ? null : { ...state.voxelBody },
222
+ motion: state.motion === null ? null : { ...state.motion },
223
+ flight: state.flight === null ? null : { ...state.flight },
224
+ controller: controller === null ? null : copyController(controller),
225
+ controllerJumpHeld: state.controllerJumpHeld,
226
+ jumpBuffer: state.jumpBuffer?.snapshot() ?? null,
227
+ heightfieldHeight: state.heightfieldHeight,
228
+ heightfieldEntityId: state.heightfieldEntityId,
229
+ };
230
+ }
231
+ /** Put a player's movement state back to a {@link snapshotPlayerMovement} copy. Pending state survives until first spawn; a driven actor's despawn clears it. */
232
+ export function restorePlayerMovement(ctx, userId, snapshot) {
233
+ const heightfieldHeight = snapshot.heightfieldHeight === undefined
234
+ ? (snapshot.motion === null ? null : DEFAULT_OBSTACLE_PLAYER_HEIGHT) : snapshot.heightfieldHeight;
235
+ if (heightfieldHeight !== null)
236
+ validateCollisionHeight(heightfieldHeight);
237
+ const heightfieldEntityId = snapshot.heightfieldEntityId === undefined
238
+ ? (heightfieldHeight === null ? null : ctx.player.possession.active(userId)) : snapshot.heightfieldEntityId;
239
+ if (heightfieldEntityId !== null && typeof heightfieldEntityId !== "string") {
240
+ throw new RangeError("PlayerMovementSnapshot.heightfieldEntityId must be a string or null.");
241
+ }
242
+ const state = stateFor(ctx, storeFor(ctx), userId);
243
+ const liveEntityId = ctx.player.possession.active(userId);
244
+ state.liveEntityId = ctx.scene.entity.get(liveEntityId) === null ? null : liveEntityId;
245
+ invalidateTelemetry(state);
246
+ state.heading = snapshot.heading;
247
+ state.facing = snapshot.facing;
248
+ state.voxelBody = snapshot.voxelBody === null ? null : { ...snapshot.voxelBody };
249
+ state.motion = snapshot.motion === null ? null : { ...snapshot.motion };
250
+ state.flight = snapshot.flight === null ? null : { ...snapshot.flight };
251
+ state.heightfieldHeight = heightfieldHeight;
252
+ state.heightfieldEntityId = heightfieldEntityId;
253
+ state.controllerJumpHeld = snapshot.controllerJumpHeld;
254
+ const jumpBuffer = snapshot.jumpBuffer ?? null;
255
+ if (jumpBuffer === null) {
256
+ state.jumpBuffer = null;
257
+ }
258
+ else {
259
+ state.jumpBuffer ??= createInputBuffer({ windowMs: jumpBuffer.windowMs });
260
+ state.jumpBuffer.restore(jumpBuffer);
261
+ }
262
+ const controller = snapshot.controller === null ? null : copyController(snapshot.controller);
263
+ if (state.controller !== null && controller !== null) {
264
+ state.controller.restore(controller);
265
+ state.pendingController = null;
266
+ }
267
+ else {
268
+ if (controller === null)
269
+ state.controller = null;
270
+ state.pendingController = controller;
271
+ }
272
+ }
84
273
  /** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
85
274
  export function forgetPlayerMovement(ctx, userId) {
86
275
  stores.get(ctx)?.players.delete(userId);
@@ -148,27 +337,81 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
148
337
  if (player.movement?.frozen === true)
149
338
  return;
150
339
  const store = storeFor(ctx);
151
- const state = stateFor(store, userId);
340
+ const retainedState = store.players.get(userId);
341
+ const state = stateFor(ctx, store, userId);
342
+ let collisionHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT;
343
+ let requestedCollisionHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT;
344
+ let collisionHeightBlocked = false;
152
345
  const held = new Set(input.held);
153
346
  const isDown = (action) => held.has(action);
347
+ let nextHeading = state.heading;
154
348
  if (heading !== undefined) {
155
- state.heading = heading;
349
+ nextHeading = heading;
156
350
  }
157
351
  else {
158
352
  const turnInput = (isDown("turnRight") ? 1 : 0) - (isDown("turnLeft") ? 1 : 0);
159
353
  const turnSpeed = tuning.movement?.turnSpeed ?? DEFAULT_TURN_SPEED;
160
354
  if (turnInput !== 0)
161
- state.heading = steerYaw(state.heading, turnInput, turnSpeed, dt);
355
+ nextHeading = steerYaw(nextHeading, turnInput, turnSpeed, dt);
162
356
  }
163
- const forwardX = Math.sin(state.heading);
164
- const forwardZ = Math.cos(state.heading);
357
+ const forwardX = Math.sin(nextHeading);
358
+ const forwardZ = Math.cos(nextHeading);
165
359
  const keys = createEmptyMovementKeys();
166
360
  keys.w = isDown("moveForward");
167
361
  keys.s = isDown("moveBack");
168
362
  keys.a = isDown("moveLeft");
169
363
  keys.d = isDown("moveRight");
170
- keys.shift = isDown("sprint") && (tuning.movement?.canSprint?.(ctx) ?? true);
171
- keys.space = isDown("jump");
364
+ let flightTuning;
365
+ try {
366
+ flightTuning = withPoseRollback(ctx.player.movement, playerId, () => {
367
+ keys.shift = isDown("sprint") && (tuning.movement?.canSprint?.(ctx) ?? true);
368
+ keys.space = isDown("jump");
369
+ keys.c = isDown("crouch") && ctx.player.movement.setPose(playerId, "crouch") === null;
370
+ if (!keys.c && tuning.controller === undefined) {
371
+ const currentPose = ctx.player.movement.getPose(playerId);
372
+ if (currentPose !== "prone")
373
+ ctx.player.movement.setPose(playerId, keys.shift ? "running" : "standing");
374
+ }
375
+ const flightTuning = flightTuningFor(tuning.movement, ctx);
376
+ if (flightTuning === null && tuning.controller === undefined && tuning.collision?.voxel !== true) {
377
+ const authoredHeight = tuning.movement?.collisionHeight;
378
+ requestedCollisionHeight = authoredHeight === undefined ? DEFAULT_OBSTACLE_PLAYER_HEIGHT : authoredHeight;
379
+ validateCollisionHeight(requestedCollisionHeight);
380
+ const previousHeight = state.heightfieldEntityId !== null && state.heightfieldEntityId !== playerId
381
+ ? null : state.heightfieldHeight;
382
+ collisionHeight = previousHeight ?? requestedCollisionHeight;
383
+ if (tuning.movement?.collideObjects !== false &&
384
+ (previousHeight === null && authoredHeight !== undefined || requestedCollisionHeight > collisionHeight)) {
385
+ const swim = tuning.movement?.swim;
386
+ const swimEnabled = swim === true || (typeof swim === "object" && swim !== null);
387
+ const floating = swimEnabled && tuning.ground.waterLevel !== undefined &&
388
+ tuning.ground.sampleHeight(player.position[0], player.position[2]) < tuning.ground.waterLevel;
389
+ const groundedSupport = state.motion?.grounded !== false && (state.motion?.jumpOffset ?? 0) <= 0 && !floating;
390
+ const supportStep = groundedSupport ? tuning.movement?.stepHeight ?? DEFAULT_PLAYER_STEP_HEIGHT : 0;
391
+ const bounds = gatherMovementObstacles(ctx, player.position, 0, 0, 0, supportStep, Math.max(collisionHeight, requestedCollisionHeight));
392
+ // An authored terrain-relative spawn can rest a few centimetres inside a walkable road plate.
393
+ // Test the same support feet the grounded motor will accept, including the resulting headroom.
394
+ const support = groundedSupport
395
+ ? obstacleSupportHeight(player.position[0], player.position[2], player.position[1], supportStep, bounds) : null;
396
+ const supportedPosition = support !== null && support > player.position[1]
397
+ ? [player.position[0], support, player.position[2]] : player.position;
398
+ const overlap = resolveObstacleVerticalStep(supportedPosition, 0, bounds, DEFAULT_OBSTACLE_PLAYER_RADIUS, requestedCollisionHeight).initialOverlap;
399
+ if (overlap && previousHeight === null) {
400
+ throw new RangeError(`PlayerMovementConfig.collisionHeight ${requestedCollisionHeight} intersects blocking geometry at entity ${playerId}. Move the authored placement or choose a height that fits.`);
401
+ }
402
+ collisionHeightBlocked = overlap;
403
+ }
404
+ if (!collisionHeightBlocked)
405
+ collisionHeight = requestedCollisionHeight;
406
+ }
407
+ return flightTuning;
408
+ });
409
+ }
410
+ catch (error) {
411
+ if (retainedState === undefined && store.players.get(userId) === state)
412
+ store.players.delete(userId);
413
+ throw error;
414
+ }
172
415
  // A frame carrying analog magnitudes (virtual joystick, gamepad stick) walks at its deflection
173
416
  // instead of slamming digital ±1 axes — the fix for "a slight stick tilt reads as a full strafe".
174
417
  const analog = input.analog ?? null;
@@ -179,9 +422,14 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
179
422
  right: (analog.moveRight ?? 0) - (analog.moveLeft ?? 0),
180
423
  };
181
424
  const intent = resolveMovementIntent(keys, true, analogMove);
182
- const motionBatch = ctx.player.motionFor(userId).takePending();
183
425
  const walkSpeed = player.movement?.walkSpeed ?? DEFAULT_WALK_SPEED;
184
- const flightTuning = flightTuningFor(tuning.movement, ctx);
426
+ if (flightTuning === null && tuning.controller === undefined && tuning.collision?.voxel !== true) {
427
+ state.heightfieldHeight = collisionHeight;
428
+ state.heightfieldEntityId = playerId;
429
+ }
430
+ state.liveEntityId = playerId;
431
+ state.heading = nextHeading;
432
+ const motionBatch = ctx.player.motionFor(userId).takePending();
185
433
  if (flightTuning !== null) {
186
434
  const value = (action) => input.analog?.[action] ?? (isDown(action) ? 1 : 0);
187
435
  let flightIntent = resolveFreeFlightIntentFromInput(isDown, value, input.pointer, flightTuning.bindings);
@@ -219,7 +467,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
219
467
  const cam = ctx.camera;
220
468
  cam?.setChaseTuning(normalizedFlightTuning.camera ?? null);
221
469
  }
222
- const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning);
470
+ const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning, { authoritativeStep: tuning.authoritativeStep });
223
471
  let stepX = step.stepX;
224
472
  let stepY = step.stepY;
225
473
  let stepZ = step.stepZ;
@@ -238,10 +486,12 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
238
486
  flightState.vy = 0;
239
487
  }
240
488
  }
489
+ let motorProposalAccepted = true;
241
490
  let nextX = player.position[0] + stepX;
242
491
  let nextY = player.position[1] + stepY;
243
492
  let nextZ = player.position[2] + stepZ;
244
493
  if (motionBatch !== null && motionBatch.y !== null) {
494
+ motorProposalAccepted = motionBatch.y === nextY;
245
495
  nextY = motionBatch.y;
246
496
  }
247
497
  if (tuning.movement?.beforeCommit !== undefined) {
@@ -254,6 +504,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
254
504
  };
255
505
  const replacement = tuning.movement.beforeCommit(frame);
256
506
  if (replacement !== undefined) {
507
+ motorProposalAccepted = motorProposalAccepted && replacement[0] === nextX && replacement[1] === nextY && replacement[2] === nextZ;
257
508
  nextX = replacement[0];
258
509
  nextY = replacement[1];
259
510
  nextZ = replacement[2];
@@ -264,6 +515,10 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
264
515
  rotationY: resolveBodyFacing(state, flightIntent.moving, flightState.vx, flightState.vz, player.rotationY, tuning.movement?.turnSpeed, dt),
265
516
  dt,
266
517
  });
518
+ if (motorProposalAccepted && positionAccepted(ctx, playerId, nextX, nextY, nextZ))
519
+ updateTelemetry(state, playerId, false, flightState.vy, false);
520
+ else
521
+ invalidateTelemetry(state);
267
522
  return;
268
523
  }
269
524
  if (flightTuning === null && state.flight !== null) {
@@ -275,7 +530,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
275
530
  if (controller === null) {
276
531
  controller = createCharacterController(tuning.controller.capsule);
277
532
  state.controller = controller;
278
- controller.restore({
533
+ controller.restore(state.pendingController ?? {
279
534
  position: [player.position[0], player.position[1], player.position[2]],
280
535
  verticalVelocity: 0,
281
536
  grounded: true,
@@ -283,7 +538,9 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
283
538
  groundBody: null,
284
539
  crouching: false,
285
540
  });
541
+ state.pendingController = null;
286
542
  }
543
+ retuneController(state, tuning.controller.capsule);
287
544
  const controllerState = controller.state();
288
545
  if (Math.hypot(controllerState.position[0] - player.position[0], controllerState.position[1] - player.position[1], controllerState.position[2] - player.position[2]) > 1e-6) {
289
546
  controller.restore({
@@ -293,43 +550,53 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
293
550
  groundBody: null,
294
551
  });
295
552
  }
296
- // Keep the existing input feel and horizontal impulse seam; the capsule owns vertical motion and collision.
297
553
  let horizontal = state.motion;
298
554
  if (horizontal === null) {
299
555
  horizontal = createPlayerMotionState();
300
556
  state.motion = horizontal;
301
557
  }
558
+ controller.setCrouch(tuning.controller.backend, intent.crouching);
302
559
  const grounded = controller.state().grounded;
303
560
  horizontal.grounded = grounded;
304
- horizontal.verticalVelocity = 0;
561
+ horizontal.verticalVelocity = applyMotionImpulses(controller.state().verticalVelocity, motionBatch);
305
562
  horizontal.jumpOffset = 0;
306
563
  [horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ] = applyHorizontalImpulses(horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, motionBatch);
307
- const jumpPressed = intent.jumping && !state.controllerJumpHeld;
308
- const horizontalStep = advancePlayerMotion(horizontal, { ...intent, jumping: false }, forwardX, forwardZ, walkSpeed, dt, tuning.physics);
309
- horizontal.grounded = grounded;
310
- horizontal.verticalVelocity = 0;
311
- horizontal.jumpOffset = 0;
564
+ const controllerIntent = {
565
+ ...intent,
566
+ crouching: controller.state().crouching,
567
+ running: intent.running && !controller.state().crouching,
568
+ };
569
+ if (state.jumpBuffer === null) {
570
+ horizontal.jumpHeld = state.controllerJumpHeld;
571
+ state.jumpBuffer = createInputBuffer({ windowMs: tuning.physics?.jumpBufferMs ?? 0 });
572
+ }
573
+ const horizontalStep = advancePlayerMotion(horizontal, controllerIntent, forwardX, forwardZ, walkSpeed, dt, tuning.physics, { buffer: state.jumpBuffer, externalGrounding: true, authoritativeStep: tuning.authoritativeStep });
312
574
  state.controllerJumpHeld = intent.jumping;
313
- const current = controller.state();
314
575
  controller.restore({
315
- ...current,
316
- verticalVelocity: applyMotionImpulses(current.verticalVelocity, motionBatch),
576
+ ...controller.state(),
577
+ verticalVelocity: horizontal.verticalVelocity,
578
+ grounded: horizontal.grounded,
317
579
  });
318
580
  const result = controller.move(tuning.controller.backend, {
319
581
  motion: [horizontalStep.stepX, 0, horizontalStep.stepZ],
320
- dt,
321
- gravity: tuning.physics?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration,
322
- ...(jumpPressed && !intent.crouching
323
- ? { jumpVelocity: tuning.physics?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity }
324
- : {}),
325
- crouch: intent.crouching,
582
+ dt: motionStepSeconds(dt, tuning.authoritativeStep),
326
583
  });
584
+ horizontal.verticalVelocity = controller.state().verticalVelocity;
585
+ horizontal.jumpOffset = 0;
586
+ if (!grounded && result.grounded)
587
+ horizontal.landedAtMs = horizontal.clockMs;
588
+ horizontal.wasAirborne = !result.grounded;
589
+ if (result.grounded)
590
+ horizontal.jumpRising = false;
591
+ ctx.player.movement.setPose(playerId, controller.state().crouching ? "crouch" : intent.running ? "running" : "standing");
592
+ let motorProposalAccepted = true;
327
593
  let nextPosition = [
328
594
  controller.state().position[0],
329
595
  controller.state().position[1],
330
596
  controller.state().position[2],
331
597
  ];
332
598
  if (motionBatch !== null && motionBatch.y !== null) {
599
+ motorProposalAccepted = motionBatch.y === nextPosition[1];
333
600
  nextPosition[1] = motionBatch.y;
334
601
  controller.restore({ ...controller.state(), position: nextPosition });
335
602
  }
@@ -343,6 +610,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
343
610
  };
344
611
  const replacement = tuning.movement.beforeCommit(frame);
345
612
  if (replacement !== undefined) {
613
+ motorProposalAccepted = motorProposalAccepted && replacement[0] === nextPosition[0] && replacement[1] === nextPosition[1] && replacement[2] === nextPosition[2];
346
614
  nextPosition = [replacement[0], replacement[1], replacement[2]];
347
615
  controller.restore({ ...controller.state(), position: nextPosition });
348
616
  }
@@ -353,6 +621,10 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
353
621
  rotationY: resolveBodyFacing(state, intent.moving, horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
354
622
  dt,
355
623
  });
624
+ if (motorProposalAccepted && positionAccepted(ctx, playerId, nextPosition[0], nextPosition[1], nextPosition[2]))
625
+ updateTelemetry(state, playerId, controller.state().grounded, controller.state().verticalVelocity, controller.state().crouching);
626
+ else
627
+ invalidateTelemetry(state);
356
628
  return;
357
629
  }
358
630
  if (tuning.collision?.voxel === true) {
@@ -375,7 +647,8 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
375
647
  };
376
648
  body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
377
649
  [body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
378
- advanceVoxelPlayer(body, intent, forwardX, forwardZ, walkSpeed, dt, isSolid, dims, tuning.physics, tuning.hasTerrain ? (x, z) => tuning.ground.sampleHeight(x, z) : undefined);
650
+ advanceVoxelPlayer(body, intent, forwardX, forwardZ, walkSpeed, dt, isSolid, dims, tuning.physics, tuning.hasTerrain ? (x, z) => tuning.ground.sampleHeight(x, z) : undefined, { authoritativeStep: tuning.authoritativeStep });
651
+ const motorProposalAccepted = motionBatch === null || motionBatch.y === null || motionBatch.y === body.y;
379
652
  if (motionBatch !== null && motionBatch.y !== null)
380
653
  body.y = motionBatch.y;
381
654
  ctx.scene.entity.setPose(playerId, {
@@ -383,6 +656,10 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
383
656
  rotationY: resolveBodyFacing(state, intent.moving, body.velocityX, body.velocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
384
657
  dt,
385
658
  });
659
+ if (motorProposalAccepted && positionAccepted(ctx, playerId, body.x, body.y, body.z))
660
+ updateTelemetry(state, playerId, body.grounded, body.velocityY, intent.crouching);
661
+ else
662
+ invalidateTelemetry(state);
386
663
  return;
387
664
  }
388
665
  let motion = state.motion;
@@ -401,10 +678,17 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
401
678
  const submerged = swimEnabled &&
402
679
  waterLevel !== undefined &&
403
680
  tuning.ground.sampleHeight(player.position[0], player.position[2]) < waterLevel;
681
+ const jumpBufferMs = tuning.physics?.jumpBufferMs ?? 0;
682
+ let jumpBuffer = state.jumpBuffer;
683
+ if (jumpBuffer === null) {
684
+ jumpBuffer = createInputBuffer({ windowMs: jumpBufferMs });
685
+ state.jumpBuffer = jumpBuffer;
686
+ }
404
687
  const motionOptions = submerged
405
- ? { speedScale: swimSpeedMultiplier, floating: true }
406
- : undefined;
688
+ ? { speedScale: swimSpeedMultiplier, floating: true, buffer: jumpBuffer, authoritativeStep: tuning.authoritativeStep }
689
+ : { buffer: jumpBuffer, externalGrounding: true, authoritativeStep: tuning.authoritativeStep };
407
690
  const prevJumpOffset = motion.jumpOffset;
691
+ const groundedBeforeStep = motion.grounded;
408
692
  const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, walkSpeed, dt, tuning.physics, motionOptions);
409
693
  // Airborne means "not resting on the surface below": any jump/impulse height before or after this
410
694
  // frame's integration. Grounded-only forgiveness (step-up) and airborne-only landing key off it.
@@ -417,11 +701,14 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
417
701
  stepZ = constrained.stepZ;
418
702
  }
419
703
  const stepHeight = tuning.movement?.stepHeight ?? DEFAULT_PLAYER_STEP_HEIGHT;
704
+ const verticalStep = airborne ? motion.jumpOffset - prevJumpOffset : 0;
705
+ const queryMinStepY = Math.min(0, verticalStep);
706
+ const queryMaxStepY = Math.max(stepHeight, verticalStep);
420
707
  let obstacles = null;
421
708
  if (tuning.movement?.collideObjects !== false) {
422
- obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ);
709
+ obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ, queryMinStepY, queryMaxStepY, collisionHeight);
423
710
  // While grounded, a box the player could simply step onto is a ledge, not a wall.
424
- const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, airborne ? 0 : stepHeight);
711
+ const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, airborne ? 0 : stepHeight, collisionHeight);
425
712
  stepX = resolved.stepX;
426
713
  stepZ = resolved.stepZ;
427
714
  }
@@ -444,46 +731,155 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
444
731
  nextZ += slope.downhill[1] * slide;
445
732
  }
446
733
  }
447
- const groundAtNext = tuning.ground.sampleHeight(nextX, nextZ);
734
+ const maxClimbGrade = tuning.movement?.maxClimbGrade;
735
+ // The full/X/Z candidates share four unique grade samples (origin and three endpoints), even
736
+ // when headroom rejects the grade resolver's first choice. Preserve the game sampler's budget.
737
+ const gradeSamples = maxClimbGrade === undefined ? null : [];
738
+ const gradeSampler = (x, z) => {
739
+ for (const sample of gradeSamples)
740
+ if (sample.x === x && sample.z === z)
741
+ return sample.height;
742
+ const authoredSampler = tuning.movement?.climbGradeHeight;
743
+ const height = authoredSampler === undefined ? tuning.ground.sampleHeight(x, z) : authoredSampler(x, z);
744
+ gradeSamples.push({ x, z, height });
745
+ return height;
746
+ };
747
+ if (maxClimbGrade !== undefined) {
748
+ const accepted = resolveTerrainGradeStep(gradeSampler, player.position, nextX - player.position[0], nextZ - player.position[2], maxClimbGrade);
749
+ nextX = player.position[0] + accepted.stepX;
750
+ nextZ = player.position[2] + accepted.stepZ;
751
+ }
448
752
  // Blocking colliders are walkable surfaces: the effective ground under the player is the higher of
449
753
  // the terrain and the tallest object top the player can stand on here. While grounded a top within
450
754
  // stepHeight above the feet is stepped onto (matching the obstruction's ledge forgiveness); while
451
755
  // airborne only tops at/below the feet catch, so a jump lands ON a crate instead of sinking inside
452
756
  // it and being rubber-banded out by depenetration.
453
- const supportY = obstacles !== null
454
- ? obstacleSupportHeight(nextX, nextZ, player.position[1], airborne ? 0 : stepHeight, obstacles)
455
- : null;
456
- const effectiveGround = supportY !== null && supportY > groundAtNext ? supportY : groundAtNext;
457
- let nextY;
458
- if (airborne) {
459
- // Integrate the jump arc in absolute space (previous feet + this frame's offset delta) so the
460
- // arc stays continuous when the ground under the player changes mid-flight, and land on the
461
- // effective ground — terrain or object top — the moment the descending feet reach it.
462
- const nextFeet = player.position[1] + (motion.jumpOffset - prevJumpOffset);
463
- if (nextFeet <= effectiveGround && motion.verticalVelocity <= 0) {
464
- nextY = effectiveGround;
465
- motion.jumpOffset = 0;
466
- motion.verticalVelocity = 0;
467
- motion.grounded = true;
468
- }
469
- else {
470
- nextY = Math.max(nextFeet, effectiveGround);
471
- motion.jumpOffset = nextY - effectiveGround;
472
- motion.grounded = false;
757
+ const absoluteHeightOverride = motionBatch?.y !== undefined && motionBatch.y !== null;
758
+ const terrainSamples = [];
759
+ const terrainHeightAt = (x, z) => {
760
+ for (const sample of terrainSamples)
761
+ if (sample.x === x && sample.z === z)
762
+ return sample.height;
763
+ const height = tuning.ground.sampleHeight(x, z);
764
+ terrainSamples.push({ x, z, height });
765
+ return height;
766
+ };
767
+ if (obstacles !== null && !absoluteHeightOverride) {
768
+ // A terrain-following style may accept a rise taller than stepHeight. Include those proposed
769
+ // full/X/Z body spans without querying the whole world or falling all the way to distant ground.
770
+ const rise = Math.max(terrainHeightAt(nextX, nextZ), terrainHeightAt(nextX, player.position[2]), terrainHeightAt(player.position[0], nextZ)) - player.position[1];
771
+ if (rise > queryMaxStepY) {
772
+ obstacles = gatherMovementObstacles(ctx, player.position, nextX - player.position[0], nextZ - player.position[2], queryMinStepY, rise, collisionHeight);
473
773
  }
474
774
  }
475
- else if (!submerged && player.position[1] - effectiveGround > stepHeight) {
476
- // Walked off a ledge taller than a step (a crate edge, a cliff): fall under gravity from here
477
- // instead of teleporting the feet down to the ground in one frame.
478
- nextY = player.position[1];
479
- motion.jumpOffset = nextY - effectiveGround;
480
- motion.verticalVelocity = 0;
481
- motion.grounded = false;
482
- }
483
- else {
484
- nextY = effectiveGround;
775
+ const validStartingBounds = obstacles === null || !resolveObstacleVerticalStep(player.position, 0, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, collisionHeight).initialOverlap;
776
+ const resolveCandidate = (x, z) => {
777
+ const terrainHeight = terrainHeightAt(x, z);
778
+ const supportY = obstacles === null ? null : obstacleSupportHeight(x, z, player.position[1], airborne ? 0 : stepHeight, obstacles);
779
+ const ground = supportY !== null && supportY > terrainHeight ? supportY : terrainHeight;
780
+ let y = ground;
781
+ let grounded = motion.grounded;
782
+ let velocity = motion.verticalVelocity;
783
+ let offset = motion.jumpOffset;
784
+ let rising = motion.jumpRising;
785
+ let blockedRise = false;
786
+ if (airborne) {
787
+ // Keep the arc in absolute space while the terrain/support changes below it.
788
+ const nextFeet = player.position[1] + (motion.jumpOffset - prevJumpOffset);
789
+ if (nextFeet <= ground && velocity <= 0) {
790
+ offset = 0;
791
+ velocity = 0;
792
+ grounded = true;
793
+ }
794
+ else {
795
+ y = Math.max(nextFeet, ground);
796
+ offset = y - ground;
797
+ grounded = false;
798
+ }
799
+ }
800
+ else if (!submerged && player.position[1] - ground > stepHeight) {
801
+ y = player.position[1];
802
+ offset = y - ground;
803
+ velocity = 0;
804
+ grounded = false;
805
+ }
806
+ if (obstacles !== null && !absoluteHeightOverride) {
807
+ const vertical = resolveObstacleVerticalStep([x, player.position[1], z], y - player.position[1], obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, collisionHeight);
808
+ if (vertical.hitCeiling) {
809
+ const clippedY = player.position[1] + vertical.stepY;
810
+ // A surface above the ceiling cannot be reached by snapping entirely through a thin roof.
811
+ blockedRise = ground > vertical.ceilingFeetY + vertical.ceilingTolerance;
812
+ y = Math.max(clippedY, ground);
813
+ offset = Math.max(0, y - ground);
814
+ velocity = 0;
815
+ rising = false;
816
+ grounded = y === ground;
817
+ }
818
+ else if (vertical.landed) {
819
+ y = player.position[1] + vertical.stepY;
820
+ offset = 0;
821
+ velocity = 0;
822
+ rising = false;
823
+ grounded = true;
824
+ }
825
+ }
826
+ const clear = obstacles === null || absoluteHeightOverride || !validStartingBounds ||
827
+ (!blockedRise && !resolveObstacleVerticalStep([x, y, z], 0, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, collisionHeight).initialOverlap);
828
+ return { x, z, y, ground, grounded, velocity, offset, rising, clear };
829
+ };
830
+ let candidate = resolveCandidate(nextX, nextZ);
831
+ if (!candidate.clear) {
832
+ const gradeAllows = (x, z) => {
833
+ if (maxClimbGrade === undefined)
834
+ return true;
835
+ const accepted = resolveTerrainGradeStep(gradeSampler, player.position, x - player.position[0], z - player.position[2], maxClimbGrade);
836
+ return accepted.stepX === x - player.position[0] && accepted.stepZ === z - player.position[2];
837
+ };
838
+ const tryAxis = (x, z) => {
839
+ if (obstacles !== null) {
840
+ // The original Z sweep used post-X coordinates. An alternate axis must be swept again from
841
+ // the original pose; reject a changed endpoint rather than accepting an unsampled grade.
842
+ const swept = resolveObstacleStep(player.position, x - player.position[0], z - player.position[2], obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, airborne ? 0 : stepHeight, collisionHeight);
843
+ if (swept.stepX !== x - player.position[0] || swept.stepZ !== z - player.position[2])
844
+ return false;
845
+ }
846
+ if (!gradeAllows(x, z))
847
+ return false;
848
+ const alternative = resolveCandidate(x, z);
849
+ if (!alternative.clear)
850
+ return false;
851
+ candidate = alternative;
852
+ return true;
853
+ };
854
+ const xAccepted = nextX !== player.position[0] && tryAxis(nextX, player.position[2]);
855
+ const zAccepted = !xAccepted && nextZ !== player.position[2] && tryAxis(player.position[0], nextZ);
856
+ if (!xAccepted && !zAccepted) {
857
+ candidate = resolveCandidate(player.position[0], player.position[2]);
858
+ if (!candidate.clear) {
859
+ // An inflated support edge cannot raise the body into a lintel. Retain the valid starting
860
+ // pose rather than forcing feet into support or repairing an illegal authored placement.
861
+ candidate.y = player.position[1];
862
+ candidate.grounded = groundedBeforeStep;
863
+ candidate.offset = prevJumpOffset;
864
+ candidate.velocity = groundedBeforeStep ? 0 : motion.verticalVelocity;
865
+ candidate.rising = groundedBeforeStep ? false : motion.jumpRising;
866
+ }
867
+ }
485
868
  }
869
+ nextX = candidate.x;
870
+ nextZ = candidate.z;
871
+ let nextY = candidate.y;
872
+ const effectiveGround = candidate.ground;
873
+ motion.grounded = candidate.grounded;
874
+ motion.verticalVelocity = candidate.velocity;
875
+ motion.jumpOffset = candidate.offset;
876
+ motion.jumpRising = candidate.rising;
877
+ let motorProposalAccepted = true;
878
+ if (!groundedBeforeStep && motion.grounded)
879
+ motion.landedAtMs = motion.clockMs;
880
+ motion.wasAirborne = !motion.grounded;
486
881
  if (motionBatch !== null && motionBatch.y !== null) {
882
+ motorProposalAccepted = motionBatch.y === nextY;
487
883
  nextY = motionBatch.y;
488
884
  motion.jumpOffset = motionBatch.y - effectiveGround;
489
885
  }
@@ -500,6 +896,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
500
896
  };
501
897
  const replacement = tuning.movement.beforeCommit(frame);
502
898
  if (replacement !== undefined) {
899
+ motorProposalAccepted = motorProposalAccepted && replacement[0] === nextX && replacement[1] === nextY && replacement[2] === nextZ;
503
900
  nextX = replacement[0];
504
901
  nextY = replacement[1];
505
902
  nextZ = replacement[2];
@@ -510,11 +907,20 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
510
907
  rotationY: resolveBodyFacing(state, intent.moving, motion.horizontalVelocityX, motion.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
511
908
  dt,
512
909
  });
910
+ if (motorProposalAccepted && positionAccepted(ctx, playerId, nextX, nextY, nextZ))
911
+ updateTelemetry(state, playerId, motion.grounded, motion.verticalVelocity, intent.crouching, collisionHeight, requestedCollisionHeight, collisionHeightBlocked);
912
+ else
913
+ invalidateTelemetry(state);
513
914
  }
514
915
  /**
515
916
  * The player's blocking obstacles for this step, from the shared solid seam. A walking NPC calls
516
917
  * `resolveWalkerStep` against the same query, so "solid" means one thing engine-wide.
517
918
  */
518
- function gatherMovementObstacles(ctx, position, stepX, stepZ) {
519
- return solidObstaclesNear(ctx, position, Math.abs(stepX) + DEFAULT_OBSTACLE_PLAYER_RADIUS, Math.abs(stepZ) + DEFAULT_OBSTACLE_PLAYER_RADIUS);
919
+ function gatherMovementObstacles(ctx, position, stepX, stepZ, minStepY = 0, maxStepY = 0, playerHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT) {
920
+ if (!Number.isFinite(minStepY) || !Number.isFinite(maxStepY))
921
+ throw new RangeError("Movement obstacle query requires a finite vertical step.");
922
+ const minY = Math.min(0, minStepY);
923
+ const maxY = Math.max(0, maxStepY);
924
+ const queryPosition = minY < 0 ? [position[0], position[1] + minY, position[2]] : position;
925
+ return solidObstaclesNear(ctx, queryPosition, Math.abs(stepX) + DEFAULT_OBSTACLE_PLAYER_RADIUS, Math.abs(stepZ) + DEFAULT_OBSTACLE_PLAYER_RADIUS, playerHeight + maxY - minY);
520
926
  }