@jgengine/core 0.18.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/ai/decisionGraph.d.ts +50 -4
  3. package/dist/ai/decisionGraph.js +159 -23
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/ai/groupAssist.d.ts +10 -0
  7. package/dist/ai/groupAssist.js +11 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/threat.d.ts +8 -0
  11. package/dist/ai/threat.js +10 -0
  12. package/dist/anim/animGraph.d.ts +43 -1
  13. package/dist/anim/animGraph.js +326 -10
  14. package/dist/anim/boneTexture.d.ts +55 -0
  15. package/dist/anim/boneTexture.js +43 -0
  16. package/dist/anim/footPlacement.d.ts +57 -0
  17. package/dist/anim/footPlacement.js +67 -0
  18. package/dist/anim/locomotionGraph.d.ts +18 -1
  19. package/dist/anim/locomotionGraph.js +42 -3
  20. package/dist/audio/audioFalloff.d.ts +6 -0
  21. package/dist/audio/doppler.d.ts +21 -0
  22. package/dist/audio/doppler.js +32 -0
  23. package/dist/audio/engineLayers.d.ts +83 -0
  24. package/dist/audio/engineLayers.js +115 -0
  25. package/dist/audio/voiceAllocator.d.ts +46 -0
  26. package/dist/audio/voiceAllocator.js +76 -0
  27. package/dist/combat/abilityKit.d.ts +20 -0
  28. package/dist/combat/abilityKit.js +41 -4
  29. package/dist/combat/death.d.ts +11 -3
  30. package/dist/combat/death.js +3 -3
  31. package/dist/combat/magazine.d.ts +9 -0
  32. package/dist/combat/magazine.js +24 -2
  33. package/dist/combat/projectiles.d.ts +94 -2
  34. package/dist/combat/projectiles.js +282 -61
  35. package/dist/combat/regenShield.d.ts +14 -0
  36. package/dist/combat/regenShield.js +23 -7
  37. package/dist/combat/shotOrigin.d.ts +6 -0
  38. package/dist/combat/shotOrigin.js +6 -0
  39. package/dist/combat/weaponFeedback.d.ts +36 -0
  40. package/dist/combat/weaponFeedback.js +42 -0
  41. package/dist/combat/weaponHandling.d.ts +161 -0
  42. package/dist/combat/weaponHandling.js +202 -0
  43. package/dist/combat/weaponPresentation.d.ts +115 -0
  44. package/dist/combat/weaponPresentation.js +96 -0
  45. package/dist/crafting/recipeCatalog.d.ts +16 -0
  46. package/dist/crafting/recipeCatalog.js +55 -0
  47. package/dist/devtools/warnOnce.d.ts +8 -0
  48. package/dist/devtools/warnOnce.js +18 -0
  49. package/dist/economy/auctionBook.d.ts +1 -1
  50. package/dist/economy/auctionBook.js +11 -1
  51. package/dist/economy/listingBook.d.ts +1 -1
  52. package/dist/economy/listingBook.js +11 -1
  53. package/dist/economy/resourceLedger.js +3 -3
  54. package/dist/economy/shopStock.js +7 -6
  55. package/dist/economy/techTree.js +7 -6
  56. package/dist/economy/unlockPoints.js +3 -6
  57. package/dist/editor/commandApply.js +89 -16
  58. package/dist/editor/commands.d.ts +38 -1
  59. package/dist/editor/commands.js +320 -3
  60. package/dist/editor/creatorStorage.d.ts +59 -0
  61. package/dist/editor/creatorStorage.js +164 -0
  62. package/dist/editor/document.d.ts +4 -0
  63. package/dist/editor/document.js +119 -18
  64. package/dist/editor/environment.js +8 -0
  65. package/dist/editor/index.d.ts +2 -0
  66. package/dist/editor/liveSync.d.ts +4 -1
  67. package/dist/editor/liveSync.js +26 -10
  68. package/dist/editor/materialAuthoring.d.ts +14 -0
  69. package/dist/editor/materialAuthoring.js +38 -0
  70. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  71. package/dist/editor/modelNodeVisibility.js +29 -0
  72. package/dist/editor/movementCatalog.d.ts +90 -0
  73. package/dist/editor/movementCatalog.js +144 -0
  74. package/dist/editor/simulation.d.ts +60 -0
  75. package/dist/editor/simulation.js +202 -0
  76. package/dist/editor/staticPrefab.d.ts +17 -0
  77. package/dist/editor/staticPrefab.js +51 -0
  78. package/dist/editor/types.d.ts +15 -0
  79. package/dist/faction/reputation.d.ts +6 -0
  80. package/dist/faction/reputation.js +11 -0
  81. package/dist/game/cameraConfig.d.ts +54 -2
  82. package/dist/game/clipRoles.d.ts +2 -1
  83. package/dist/game/clipRoles.js +7 -3
  84. package/dist/game/contentValidation.d.ts +69 -0
  85. package/dist/game/contentValidation.js +118 -0
  86. package/dist/game/controlGate.d.ts +8 -0
  87. package/dist/game/controlGate.js +43 -6
  88. package/dist/game/defineGame.d.ts +32 -3
  89. package/dist/game/dialogueGraph.d.ts +20 -0
  90. package/dist/game/dialogueGraph.js +107 -0
  91. package/dist/game/events.d.ts +11 -0
  92. package/dist/game/keyValueStore.d.ts +17 -4
  93. package/dist/game/keyValueStore.js +35 -23
  94. package/dist/game/lootModifiers.d.ts +17 -0
  95. package/dist/game/lootModifiers.js +26 -0
  96. package/dist/game/lootTable.d.ts +4 -0
  97. package/dist/game/lootTable.js +16 -0
  98. package/dist/game/narrativeAuthoring.d.ts +67 -0
  99. package/dist/game/narrativeAuthoring.js +122 -0
  100. package/dist/game/playableGame.d.ts +134 -35
  101. package/dist/game/quest.d.ts +15 -0
  102. package/dist/game/quest.js +209 -63
  103. package/dist/game/questCatalog.d.ts +50 -0
  104. package/dist/game/questCatalog.js +205 -0
  105. package/dist/game/saveStore.d.ts +6 -5
  106. package/dist/game/saveStore.js +57 -57
  107. package/dist/game/systemRuntime.d.ts +2 -2
  108. package/dist/game/systemRuntime.js +84 -33
  109. package/dist/game/toasts.d.ts +8 -0
  110. package/dist/game/toasts.js +7 -0
  111. package/dist/game/vfxInstance.d.ts +9 -0
  112. package/dist/game/vfxInstance.js +12 -1
  113. package/dist/game/viewports.d.ts +43 -0
  114. package/dist/game/viewports.js +46 -0
  115. package/dist/game/worldItem.d.ts +5 -1
  116. package/dist/game/worldItem.js +0 -1
  117. package/dist/gameplay.d.ts +3 -0
  118. package/dist/gameplay.js +3 -0
  119. package/dist/input/actionBindings.d.ts +15 -1
  120. package/dist/input/actionBindings.js +14 -3
  121. package/dist/input/actionContexts.d.ts +25 -1
  122. package/dist/input/actionContexts.js +82 -25
  123. package/dist/input/axisInput.d.ts +4 -0
  124. package/dist/input/axisInput.js +4 -0
  125. package/dist/input/axisShaper.d.ts +73 -0
  126. package/dist/input/axisShaper.js +105 -0
  127. package/dist/input/gamepadModel.d.ts +36 -2
  128. package/dist/input/gamepadModel.js +69 -30
  129. package/dist/input/haptics.d.ts +50 -0
  130. package/dist/input/haptics.js +74 -0
  131. package/dist/input/inputBuffer.d.ts +40 -0
  132. package/dist/input/inputBuffer.js +59 -0
  133. package/dist/input/lookChannel.d.ts +10 -0
  134. package/dist/input/lookChannel.js +6 -0
  135. package/dist/input/pointer.d.ts +2 -0
  136. package/dist/input/pointer.js +4 -0
  137. package/dist/interaction/promptRegistry.d.ts +5 -4
  138. package/dist/interaction/promptRegistry.js +8 -2
  139. package/dist/inventory/inventoryModel.js +9 -6
  140. package/dist/inventory/shapedGrid.d.ts +48 -9
  141. package/dist/inventory/shapedGrid.js +69 -10
  142. package/dist/item/affix.d.ts +6 -0
  143. package/dist/item/affix.js +6 -0
  144. package/dist/item/durability.js +4 -0
  145. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  146. package/dist/item/itemInstanceRegistry.js +111 -7
  147. package/dist/item/itemgen.d.ts +1 -1
  148. package/dist/item/itemgen.js +1 -1
  149. package/dist/item/modularItem.js +7 -1
  150. package/dist/material/appearanceSignals.d.ts +53 -0
  151. package/dist/material/appearanceSignals.js +47 -0
  152. package/dist/material/hairCards.d.ts +47 -0
  153. package/dist/material/hairCards.js +136 -0
  154. package/dist/material/materialAsset.d.ts +162 -0
  155. package/dist/material/materialAsset.js +382 -0
  156. package/dist/meta/changelog.d.ts +1 -1
  157. package/dist/meta/changelog.js +444 -1
  158. package/dist/movement/characterController.d.ts +5 -2
  159. package/dist/movement/characterController.js +50 -23
  160. package/dist/movement/freeFlight.d.ts +2 -2
  161. package/dist/movement/freeFlight.js +3 -3
  162. package/dist/movement/glideModel.d.ts +3 -0
  163. package/dist/movement/glideModel.js +6 -0
  164. package/dist/movement/grappleSwing.d.ts +2 -0
  165. package/dist/movement/grappleSwing.js +5 -1
  166. package/dist/movement/movementModel.d.ts +63 -1
  167. package/dist/movement/movementModel.js +136 -26
  168. package/dist/movement/movementProbe.d.ts +51 -0
  169. package/dist/movement/movementProbe.js +119 -0
  170. package/dist/movement/playerMovement.d.ts +48 -2
  171. package/dist/movement/playerMovement.js +493 -87
  172. package/dist/movement/poseState.d.ts +2 -0
  173. package/dist/movement/poseState.js +29 -1
  174. package/dist/movement/solidObstacles.d.ts +11 -1
  175. package/dist/movement/solidObstacles.js +48 -3
  176. package/dist/movement/terrainGrade.d.ts +13 -0
  177. package/dist/movement/terrainGrade.js +33 -0
  178. package/dist/movement/voxelController.d.ts +2 -2
  179. package/dist/movement/voxelController.js +10 -10
  180. package/dist/nav/navFromEnvironment.d.ts +9 -3
  181. package/dist/nav/navFromEnvironment.js +18 -12
  182. package/dist/nav/navGrid.d.ts +19 -1
  183. package/dist/nav/navGrid.js +26 -10
  184. package/dist/nav/navMesh.d.ts +43 -2
  185. package/dist/nav/navMesh.js +534 -128
  186. package/dist/nav/solidRoute.d.ts +50 -0
  187. package/dist/nav/solidRoute.js +144 -0
  188. package/dist/physics/aircraftDynamics.d.ts +390 -0
  189. package/dist/physics/aircraftDynamics.js +704 -0
  190. package/dist/physics/ballisticSweep.d.ts +10 -3
  191. package/dist/physics/ballisticSweep.js +53 -29
  192. package/dist/physics/boatDynamics.d.ts +116 -0
  193. package/dist/physics/boatDynamics.js +160 -0
  194. package/dist/physics/drivableVehicle.d.ts +22 -8
  195. package/dist/physics/drivableVehicle.js +4 -2
  196. package/dist/physics/flightDynamics.d.ts +21 -4
  197. package/dist/physics/flightDynamics.js +28 -5
  198. package/dist/physics/forceVolume.d.ts +41 -0
  199. package/dist/physics/forceVolume.js +88 -0
  200. package/dist/physics/handlingProbe.d.ts +249 -0
  201. package/dist/physics/handlingProbe.js +606 -0
  202. package/dist/physics/kinematicVehicle.d.ts +40 -5
  203. package/dist/physics/kinematicVehicle.js +17 -2
  204. package/dist/physics/physicsBackend.d.ts +12 -0
  205. package/dist/physics/physicsBackendConformance.js +34 -0
  206. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  207. package/dist/physics/physicsWorldBackend.js +52 -3
  208. package/dist/physics/spatialGrid.d.ts +1 -1
  209. package/dist/physics/spatialGrid.js +8 -9
  210. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  211. package/dist/physics/vehicleBackendLink.js +102 -0
  212. package/dist/physics/vehicleDynamics.d.ts +375 -0
  213. package/dist/physics/vehicleDynamics.js +737 -0
  214. package/dist/physics/vehicleObstacles.d.ts +3 -2
  215. package/dist/physics/vehicleObstacles.js +29 -5
  216. package/dist/physics/worldColliders.d.ts +1 -1
  217. package/dist/physics/worldColliders.js +35 -3
  218. package/dist/puzzle/cellGrid.d.ts +15 -9
  219. package/dist/puzzle/cellGrid.js +13 -9
  220. package/dist/render/lookPreset.d.ts +9 -2
  221. package/dist/render/lookPreset.js +29 -2
  222. package/dist/render/postProcessing.d.ts +30 -0
  223. package/dist/render/postProcessing.js +1 -1
  224. package/dist/runtime/cameraDirector.d.ts +54 -3
  225. package/dist/runtime/cameraDirector.js +187 -0
  226. package/dist/runtime/commandDispatch.d.ts +14 -0
  227. package/dist/runtime/commandDispatch.js +19 -0
  228. package/dist/runtime/commandInput.d.ts +37 -0
  229. package/dist/runtime/commandInput.js +58 -0
  230. package/dist/runtime/commandRunner.d.ts +27 -0
  231. package/dist/runtime/commandRunner.js +31 -3
  232. package/dist/runtime/context/combat.d.ts +12 -4
  233. package/dist/runtime/context/combat.js +179 -11
  234. package/dist/runtime/context/deathLoot.d.ts +11 -8
  235. package/dist/runtime/context/deathLoot.js +17 -16
  236. package/dist/runtime/context/scene.d.ts +2 -0
  237. package/dist/runtime/context/scene.js +13 -21
  238. package/dist/runtime/descriptors/baseline.js +34 -3
  239. package/dist/runtime/descriptors/features.d.ts +2 -1
  240. package/dist/runtime/descriptors/features.js +18 -0
  241. package/dist/runtime/gameContext.js +86 -13
  242. package/dist/runtime/gameContextTypes.d.ts +33 -3
  243. package/dist/runtime/gameRuntime.d.ts +3 -1
  244. package/dist/runtime/gameRuntime.js +4 -0
  245. package/dist/runtime/headlessRunner.d.ts +3 -1
  246. package/dist/runtime/headlessRunner.js +11 -11
  247. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  248. package/dist/runtime/hostedGameRunner.js +156 -10
  249. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  250. package/dist/runtime/hostedWorldSession.js +154 -16
  251. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  252. package/dist/runtime/inputRecorder.d.ts +1 -1
  253. package/dist/runtime/inputRecorder.js +21 -1
  254. package/dist/runtime/inputSnapshot.d.ts +22 -4
  255. package/dist/runtime/inputSnapshot.js +41 -3
  256. package/dist/runtime/localPlayers.d.ts +73 -0
  257. package/dist/runtime/localPlayers.js +119 -0
  258. package/dist/runtime/runtimeSave.d.ts +10 -6
  259. package/dist/runtime/runtimeSave.js +11 -5
  260. package/dist/runtime/simContext.d.ts +8 -3
  261. package/dist/runtime/simContext.js +7 -3
  262. package/dist/runtime/simSnapshot.js +2 -0
  263. package/dist/runtime/transport.d.ts +9 -0
  264. package/dist/runtime/transport.js +7 -1
  265. package/dist/runtime/worldReplication.d.ts +2 -0
  266. package/dist/runtime/worldReplication.js +1 -1
  267. package/dist/scene/assetCatalog.d.ts +2 -0
  268. package/dist/scene/behaviorRuntime.d.ts +38 -7
  269. package/dist/scene/behaviorRuntime.js +293 -30
  270. package/dist/scene/behaviors.d.ts +35 -2
  271. package/dist/scene/behaviors.js +29 -0
  272. package/dist/scene/entityStore.d.ts +14 -3
  273. package/dist/scene/entityStore.js +45 -9
  274. package/dist/scene/mount.d.ts +9 -0
  275. package/dist/scene/mount.js +35 -0
  276. package/dist/scene/possession.d.ts +2 -0
  277. package/dist/scene/possession.js +21 -1
  278. package/dist/scene/sceneKinds.d.ts +9 -0
  279. package/dist/scene/sceneRaycast.d.ts +7 -0
  280. package/dist/scene/sceneRaycast.js +55 -0
  281. package/dist/scene/spatial.d.ts +20 -1
  282. package/dist/scene/spatial.js +94 -2
  283. package/dist/scene/vehicleSeat.d.ts +41 -3
  284. package/dist/scene/vehicleSeat.js +94 -3
  285. package/dist/sensor/concealment.d.ts +6 -0
  286. package/dist/sensor/concealment.js +8 -0
  287. package/dist/sensor/freezeMonitor.d.ts +6 -0
  288. package/dist/sensor/freezeMonitor.js +8 -0
  289. package/dist/sensor/perception.d.ts +11 -1
  290. package/dist/sensor/perception.js +57 -10
  291. package/dist/sensor/recordingBuffer.d.ts +3 -0
  292. package/dist/sensor/recordingBuffer.js +8 -0
  293. package/dist/session/roundState.d.ts +4 -0
  294. package/dist/session/roundState.js +11 -0
  295. package/dist/settings/frameRateLimit.d.ts +16 -0
  296. package/dist/settings/frameRateLimit.js +37 -0
  297. package/dist/settings/graphicsProfile.d.ts +2 -0
  298. package/dist/settings/graphicsProfile.js +12 -0
  299. package/dist/settings/graphicsSettings.d.ts +40 -0
  300. package/dist/settings/graphicsSettings.js +81 -0
  301. package/dist/settings/settingsModel.d.ts +9 -1
  302. package/dist/settings/settingsModel.js +10 -0
  303. package/dist/stats/accumulatorMeter.d.ts +10 -0
  304. package/dist/stats/accumulatorMeter.js +6 -0
  305. package/dist/stats/eventMeter.d.ts +10 -3
  306. package/dist/stats/eventMeter.js +6 -3
  307. package/dist/store/defineStore.d.ts +3 -1
  308. package/dist/store/defineStore.js +3 -1
  309. package/dist/store/observableKeyedStore.d.ts +1 -1
  310. package/dist/store/observableKeyedStore.js +8 -6
  311. package/dist/time/simClock.d.ts +6 -2
  312. package/dist/time/simClock.js +5 -0
  313. package/dist/vfx/feedbackMixer.d.ts +61 -0
  314. package/dist/vfx/feedbackMixer.js +106 -0
  315. package/dist/vfx/particleDirector.d.ts +58 -6
  316. package/dist/vfx/particleDirector.js +113 -22
  317. package/dist/vfx/particles.d.ts +68 -5
  318. package/dist/vfx/particles.js +284 -32
  319. package/dist/visibility/assetStreaming.d.ts +21 -3
  320. package/dist/visibility/assetStreaming.js +108 -24
  321. package/dist/visibility/settings.d.ts +8 -2
  322. package/dist/visibility/settings.js +3 -0
  323. package/dist/visibility/simulationCulling.d.ts +8 -1
  324. package/dist/visibility/simulationCulling.js +7 -0
  325. package/dist/visibility/spatialIndex.d.ts +21 -0
  326. package/dist/visibility/spatialIndex.js +16 -0
  327. package/dist/visibility/visibilitySystem.js +1 -0
  328. package/dist/work/expedition.d.ts +110 -0
  329. package/dist/work/expedition.js +195 -0
  330. package/dist/work/staffedStation.d.ts +29 -0
  331. package/dist/work/staffedStation.js +27 -0
  332. package/dist/world/authoredAnimation.d.ts +38 -0
  333. package/dist/world/authoredAnimation.js +99 -0
  334. package/dist/world/authoredObjects.d.ts +24 -4
  335. package/dist/world/authoredObjects.js +43 -16
  336. package/dist/world/authoredSimulation.d.ts +74 -0
  337. package/dist/world/authoredSimulation.js +302 -0
  338. package/dist/world/authoredSolids.d.ts +46 -0
  339. package/dist/world/authoredSolids.js +75 -0
  340. package/dist/world/authoredWeather.d.ts +120 -0
  341. package/dist/world/authoredWeather.js +314 -0
  342. package/dist/world/buildingGenerator.js +2 -2
  343. package/dist/world/buildingKit.d.ts +11 -4
  344. package/dist/world/buildingKit.js +21 -8
  345. package/dist/world/buildings.d.ts +34 -2
  346. package/dist/world/buildings.js +11 -1
  347. package/dist/world/cityKind.d.ts +7 -0
  348. package/dist/world/cityKind.js +52 -1
  349. package/dist/world/environmentForces.d.ts +21 -0
  350. package/dist/world/environmentForces.js +42 -0
  351. package/dist/world/features.d.ts +32 -5
  352. package/dist/world/features.js +6 -0
  353. package/dist/world/footprintGrid.d.ts +62 -0
  354. package/dist/world/footprintGrid.js +217 -0
  355. package/dist/world/interiors.d.ts +1 -0
  356. package/dist/world/interiors.js +5 -3
  357. package/dist/world/lod.d.ts +6 -0
  358. package/dist/world/lod.js +8 -0
  359. package/dist/world/pathTerrain.d.ts +1 -1
  360. package/dist/world/terraform.d.ts +9 -1
  361. package/dist/world/terraform.js +53 -18
  362. package/dist/world/walls.d.ts +7 -0
  363. package/dist/world/walls.js +7 -0
  364. package/dist/world/weather.d.ts +17 -0
  365. package/dist/world/weather.js +116 -20
  366. package/dist/world/worldSolids.d.ts +95 -0
  367. package/dist/world/worldSolids.js +351 -0
  368. package/dist/world.d.ts +6 -6
  369. package/dist/world.js +5 -5
  370. package/package.json +1 -1
  371. package/skills/ce-handoff/SKILL.md +2 -2
  372. package/skills/game-design/SKILL.md +2 -2
  373. package/skills/jgengine/SKILL.md +28 -6
  374. package/skills/jgengine/api.md +139 -99
  375. package/skills/jgengine/capabilities.md +28 -0
  376. package/skills/jgengine/recipes/minimal-game.md +10 -2
  377. package/skills/jgengine-combat/SKILL.md +13 -2
  378. package/skills/jgengine-combat/api.md +150 -103
  379. package/skills/jgengine-combat/capabilities.md +29 -1
  380. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  381. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  382. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  383. package/skills/jgengine-combat/reference.md +24 -2
  384. package/skills/jgengine-gameplay/SKILL.md +63 -0
  385. package/skills/jgengine-gameplay/api.md +583 -463
  386. package/skills/jgengine-gameplay/capabilities.md +92 -0
  387. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  388. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  389. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  390. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  391. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  392. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  393. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  394. package/skills/jgengine-gameplay/reference-systems.md +123 -0
  395. package/skills/jgengine-world/SKILL.md +25 -4
  396. package/skills/jgengine-world/api.md +1045 -655
  397. package/skills/jgengine-world/capabilities.md +246 -9
  398. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  399. package/skills/jgengine-world/recipes/controls.md +48 -0
  400. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  401. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  402. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  403. package/skills/jgengine-world/reference.md +180 -4
  404. package/skills/jgengine-world/visibility.md +139 -0
@@ -2,7 +2,7 @@ import type { BodyHandle, BodyShape, PhysicsBackend, PhysicsVec3 } from "../phys
2
2
  /** Capsule dimensions and the walk rules a controller applies; every field is retunable during play. */
3
3
  export interface CharacterControllerConfig {
4
4
  radius: number;
5
- /** Standing height from feet to crown. */
5
+ /** Standing height from feet to crown, at least twice `radius`. */
6
6
  height: number;
7
7
  /** Height the feet may pop up to clear a ledge in one move. Default 0.35. */
8
8
  stepHeight?: number;
@@ -10,7 +10,7 @@ export interface CharacterControllerConfig {
10
10
  maxSlopeDeg?: number;
11
11
  /** Gap kept between the capsule and any surface so casts never start inside geometry. Default 0.02. */
12
12
  skinWidth?: number;
13
- /** Height while crouched. Default `height * 0.6`. */
13
+ /** Height while crouched, between the capsule diameter and standing height. Default max(`height * 0.6`, diameter). */
14
14
  crouchHeight?: number;
15
15
  /** How far below the feet the ground may be while still counting as grounded (stairs down, small bumps). Default 0.3. */
16
16
  snapDistance?: number;
@@ -64,12 +64,15 @@ export interface CharacterController {
64
64
  shape(): BodyShape;
65
65
  /** Capsule center for the current state. */
66
66
  center(): [number, number, number];
67
+ /** Set stance before resolving movement intent. Returns false only when standing lacks headroom. */
68
+ setCrouch(backend: PhysicsBackend, crouch: boolean, self?: BodyHandle): boolean;
67
69
  move(backend: PhysicsBackend, input: CharacterMoveInput): CharacterMoveResult;
68
70
  }
69
71
  /**
70
72
  * Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
71
73
  * ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
72
74
  * the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
75
+ * Invalid dimensions or control ranges throw before configuration changes.
73
76
  *
74
77
  * @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
75
78
  */
@@ -1,25 +1,47 @@
1
1
  function resolve(config) {
2
- return {
2
+ const cfg = {
3
3
  radius: config.radius,
4
4
  height: config.height,
5
5
  stepHeight: config.stepHeight ?? 0.35,
6
6
  maxSlopeDeg: config.maxSlopeDeg ?? 50,
7
7
  skinWidth: config.skinWidth ?? 0.02,
8
- crouchHeight: config.crouchHeight ?? config.height * 0.6,
8
+ crouchHeight: config.crouchHeight ?? Math.max(config.radius * 2, config.height * 0.6),
9
9
  snapDistance: config.snapDistance ?? 0.3,
10
10
  maxSlides: config.maxSlides ?? 4,
11
11
  mask: config.mask ?? 0xffffffff,
12
12
  };
13
+ for (const field of ["radius", "height", "crouchHeight"]) {
14
+ if (!Number.isFinite(cfg[field]) || cfg[field] <= 0)
15
+ throw new RangeError(`CharacterController.${field} must be finite and greater than zero.`);
16
+ }
17
+ const diameter = cfg.radius * 2;
18
+ if (cfg.height < diameter)
19
+ throw new RangeError(`CharacterController.height must be at least the capsule diameter (${diameter}). Increase height or reduce radius.`);
20
+ if (cfg.crouchHeight < diameter)
21
+ throw new RangeError(`CharacterController.crouchHeight must be at least the capsule diameter (${diameter}). Increase crouchHeight or reduce radius.`);
22
+ if (cfg.crouchHeight > cfg.height)
23
+ throw new RangeError("CharacterController.crouchHeight must not exceed standing height.");
24
+ for (const field of ["stepHeight", "skinWidth", "snapDistance"]) {
25
+ if (!Number.isFinite(cfg[field]) || cfg[field] < 0)
26
+ throw new RangeError(`CharacterController.${field} must be finite and nonnegative.`);
27
+ }
28
+ if (!Number.isFinite(cfg.maxSlopeDeg) || cfg.maxSlopeDeg < 0 || cfg.maxSlopeDeg >= 90)
29
+ throw new RangeError("CharacterController.maxSlopeDeg must be finite and between 0 (inclusive) and 90 (exclusive).");
30
+ if (!Number.isInteger(cfg.maxSlides) || cfg.maxSlides < 1)
31
+ throw new RangeError("CharacterController.maxSlides must be a positive integer.");
32
+ return cfg;
13
33
  }
14
34
  /**
15
35
  * Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
16
36
  * ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
17
37
  * the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
38
+ * Invalid dimensions or control ranges throw before configuration changes.
18
39
  *
19
40
  * @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
20
41
  */
21
42
  export function createCharacterController(initial) {
22
- let cfg = resolve(initial);
43
+ let declaration = { ...initial };
44
+ let cfg = resolve(declaration);
23
45
  let state = {
24
46
  position: [0, 0, 0],
25
47
  verticalVelocity: 0,
@@ -69,10 +91,26 @@ export function createCharacterController(initial) {
69
91
  pos[2] += motion[2] * fraction;
70
92
  return { fraction, hit };
71
93
  }
94
+ function setCrouch(backend, crouch, self) {
95
+ if (crouch === state.crouching)
96
+ return true;
97
+ if (!crouch && backend.overlap({
98
+ shape: shapeFor(cfg.height),
99
+ position: centerFor(state.position, cfg.height),
100
+ mask: cfg.mask,
101
+ ...(self === undefined ? {} : { exclude: self }),
102
+ }).length > 0)
103
+ return false;
104
+ state.crouching = crouch;
105
+ return true;
106
+ }
72
107
  return {
73
108
  config: () => cfg,
74
109
  retune(next) {
75
- cfg = resolve({ ...cfg, ...next });
110
+ const updated = { ...declaration, ...next };
111
+ const resolved = resolve(updated);
112
+ declaration = updated;
113
+ cfg = resolved;
76
114
  },
77
115
  state: () => state,
78
116
  snapshot: () => ({
@@ -95,6 +133,7 @@ export function createCharacterController(initial) {
95
133
  },
96
134
  shape: () => shapeFor(currentHeight()),
97
135
  center: () => centerFor(state.position, currentHeight()),
136
+ setCrouch,
98
137
  move(backend, input) {
99
138
  const { dt, self } = input;
100
139
  const result = {
@@ -108,21 +147,8 @@ export function createCharacterController(initial) {
108
147
  };
109
148
  const start = [state.position[0], state.position[1], state.position[2]];
110
149
  const pos = [start[0], start[1], start[2]];
111
- if (input.crouch === true && !state.crouching) {
112
- state.crouching = true;
113
- }
114
- else if (input.crouch === false && state.crouching) {
115
- const standing = backend.overlap({
116
- shape: shapeFor(cfg.height),
117
- position: centerFor(pos, cfg.height),
118
- mask: cfg.mask,
119
- ...(self === undefined ? {} : { exclude: self }),
120
- });
121
- if (standing.length === 0)
122
- state.crouching = false;
123
- else
124
- result.crouchBlocked = true;
125
- }
150
+ if (input.crouch !== undefined)
151
+ result.crouchBlocked = !setCrouch(backend, input.crouch, self);
126
152
  const height = currentHeight();
127
153
  let carry = [0, 0, 0];
128
154
  if (state.grounded && state.groundBody !== null) {
@@ -145,7 +171,8 @@ export function createCharacterController(initial) {
145
171
  if (!walkable(hit.normal)) {
146
172
  result.hitWall = true;
147
173
  slid[1] = 0;
148
- if (cfg.stepHeight > 0 && !result.steppedUp && tryStepUp(backend, pos, height, remaining, self)) {
174
+ if (state.grounded && state.verticalVelocity <= 0 && input.jumpVelocity === undefined &&
175
+ cfg.stepHeight > 0 && !result.steppedUp && tryStepUp(backend, pos, height, remaining, self)) {
149
176
  result.steppedUp = true;
150
177
  result.hitWall = false;
151
178
  break;
@@ -178,9 +205,9 @@ export function createCharacterController(initial) {
178
205
  state.groundBody = verticalHit.body;
179
206
  if (state.verticalVelocity < 0)
180
207
  state.verticalVelocity = 0;
181
- // A contact at t=0 means touching or already inside; lift by the skin so the next casts start clear.
182
- if (verticalHit.toi <= 0)
183
- pos[1] += cfg.skinWidth;
208
+ const initialGap = Math.max(0, verticalHit.toi * Math.abs(vertical));
209
+ if (initialGap < cfg.skinWidth)
210
+ pos[1] += cfg.skinWidth - initialGap;
184
211
  }
185
212
  else {
186
213
  state.grounded = false;
@@ -1,4 +1,4 @@
1
- import { type AnalogMoveIntent, type CollisionObstacle, type MovementKeysState } from "./movementModel.js";
1
+ import { type AnalogMoveIntent, type CollisionObstacle, type MovementKeysState, type MotionFrameOptions } from "./movementModel.js";
2
2
  import { type AxisBinding } from "../input/axisInput.js";
3
3
  import type { PointerAxisState } from "../input/pointerAxis.js";
4
4
  import type { ChaseCameraTuning } from "../runtime/cameraDirector.js";
@@ -91,7 +91,7 @@ export declare function resolveFreeFlightIntentFromInput(isDown: (action: string
91
91
  * Horizontal uses yaw-relative strafe (A = left, D = right) — never roll/bank.
92
92
  * Vertical is independent of pitch unless `alignWithLook` is true (spectator).
93
93
  */
94
- export declare function advanceFreeFlight(state: FreeFlightState, intent: FreeFlightIntent, yaw: number, pitch: number | undefined, dt: number, tuning: FreeFlightTuning): FreeFlightStep;
94
+ export declare function advanceFreeFlight(state: FreeFlightState, intent: FreeFlightIntent, yaw: number, pitch: number | undefined, dt: number, tuning: FreeFlightTuning, options?: Pick<MotionFrameOptions, "authoritativeStep">): FreeFlightStep;
95
95
  /** Resolve a flight step against world solids (XZ slide + Y clamp). */
96
96
  export declare function resolveFlightStep(position: readonly [number, number, number], stepX: number, stepY: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius?: number): {
97
97
  stepX: number;
@@ -1,4 +1,4 @@
1
- import { MOVEMENT_TUNING } from "./movementModel.js";
1
+ import { motionStepSeconds } from "./movementModel.js";
2
2
  import { DEFAULT_OBSTACLE_PLAYER_RADIUS } from "./movementModel.js";
3
3
  import { sampleAxisBindings } from "../input/axisInput.js";
4
4
  const ANALOG_EPSILON = 0.02;
@@ -50,8 +50,8 @@ export function resolveFreeFlightIntentFromInput(isDown, value, pointer, binding
50
50
  * Horizontal uses yaw-relative strafe (A = left, D = right) — never roll/bank.
51
51
  * Vertical is independent of pitch unless `alignWithLook` is true (spectator).
52
52
  */
53
- export function advanceFreeFlight(state, intent, yaw, pitch, dt, tuning) {
54
- const delta = Math.min(dt, MOVEMENT_TUNING.maxFrameSeconds);
53
+ export function advanceFreeFlight(state, intent, yaw, pitch, dt, tuning, options) {
54
+ const delta = motionStepSeconds(dt, options?.authoritativeStep);
55
55
  if (delta <= 0)
56
56
  return { stepX: 0, stepY: 0, stepZ: 0 };
57
57
  const speed = tuning.speed ?? 8;
@@ -40,6 +40,9 @@ export interface GlideModel {
40
40
  step(dt: number, input?: GlideInput, externalVelocity?: GlideVec3): GlideStep;
41
41
  pose(): GlideStep;
42
42
  launch(position: GlideVec3, heading: number, initialSpeed?: number): void;
43
+ /** Plain JSON copy of the glider's position, velocity and heading. */
44
+ snapshot(): GlideStep;
45
+ restore(next: GlideStep): void;
43
46
  }
44
47
  /**
45
48
  * Gliding/wingsuit descent control — lift, drag, and steering from a launch.
@@ -32,6 +32,12 @@ export function createGlideModel(config = {}) {
32
32
  vz = Math.cos(heading) * initialSpeed;
33
33
  },
34
34
  pose: snapshot,
35
+ snapshot,
36
+ restore(next) {
37
+ [x, y, z] = next.position;
38
+ [vx, vy, vz] = next.velocity;
39
+ heading = next.heading;
40
+ },
35
41
  step(dt, input = {}, externalVelocity) {
36
42
  if (dt <= 0)
37
43
  return snapshot();
@@ -28,6 +28,8 @@ export interface GrappleSwing {
28
28
  fire(anchor: SwingVec3, from: SwingVec3): void;
29
29
  release(): void;
30
30
  state(): GrappleSwingState;
31
+ /** Puts a {@link GrappleSwing.state} back; `anchor: null` detaches. */
32
+ restore(next: GrappleSwingState): void;
31
33
  /** Constrain one frame. `reeling` shortens the rope; returns the corrected position/velocity. */
32
34
  step(position: SwingVec3, velocity: SwingVec3, dt: number, reeling?: boolean): GrappleSwingStep;
33
35
  }
@@ -18,7 +18,11 @@ export function createGrappleSwing(config = {}) {
18
18
  release() {
19
19
  anchor = null;
20
20
  },
21
- state: () => ({ attached: anchor !== null, anchor, ropeLength }),
21
+ state: () => ({ attached: anchor !== null, anchor: anchor === null ? null : [anchor[0], anchor[1], anchor[2]], ropeLength }),
22
+ restore(next) {
23
+ anchor = next.anchor === null ? null : [next.anchor[0], next.anchor[1], next.anchor[2]];
24
+ ropeLength = next.ropeLength;
25
+ },
22
26
  step(position, velocity, dt, reeling = false) {
23
27
  if (anchor === null || dt <= 0)
24
28
  return { position, velocity };
@@ -1,3 +1,4 @@
1
+ import type { InputBuffer } from "../input/inputBuffer.js";
1
2
  /**
2
3
  * Movement core (pure model).
3
4
  *
@@ -83,6 +84,16 @@ export interface PlayerMotionState {
83
84
  jumpOffset: number;
84
85
  grounded: boolean;
85
86
  jumpHeld: boolean;
87
+ /** Integrated time, ms; the clock jump buffering and coyote time read. */
88
+ clockMs: number;
89
+ /** `clockMs` of the last grounded frame; `null` after a jump spends it. */
90
+ groundedAtMs: number | null;
91
+ /** `clockMs` of the last landing; `null` before the first. */
92
+ landedAtMs: number | null;
93
+ /** Rising from the player's own jump, so releasing jump may cut it. */
94
+ jumpRising: boolean;
95
+ /** Ended the last frame in the air. */
96
+ wasAirborne: boolean;
86
97
  }
87
98
  /** @internal */
88
99
  export declare function createPlayerMotionState(): PlayerMotionState;
@@ -100,6 +111,32 @@ export interface MovementTuningOverrides {
100
111
  jumpVelocity?: number;
101
112
  /** Fraction of walk speed while backpedalling; overrides {@link MOVEMENT_TUNING.backpedalSpeedMultiplier}. */
102
113
  backpedalSpeedMultiplier?: number;
114
+ /** Ground velocity response, 1/s: how fast the body reaches its target velocity (default 26). */
115
+ groundAcceleration?: number;
116
+ /** Air velocity response, 1/s: air control (default 12; `0` locks the jump arc). */
117
+ airAcceleration?: number;
118
+ /** Ground stopping response with no input, 1/s (default 18; lower slides). */
119
+ groundFriction?: number;
120
+ /** Sprint speed as a multiple of walk speed (default 2.25). */
121
+ runSpeedMultiplier?: number;
122
+ /** Crouch speed as a multiple of walk speed (default 0.45). */
123
+ crouchSpeedMultiplier?: number;
124
+ /** A jump pressed up to this many ms before landing still fires on landing (default 0). Needs {@link MotionFrameOptions.buffer}. */
125
+ jumpBufferMs?: number;
126
+ /** A jump pressed up to this many ms after walking off a ledge still fires (default 0). */
127
+ coyoteMs?: number;
128
+ /** Rising speed kept when jump is released early, 0..1 (default 1: no cut). Lower makes a tap a hop. */
129
+ jumpCutFactor?: number;
130
+ /** Gravity multiplier near the peak, while vertical speed is under {@link MovementTuningOverrides.apexSpeed} (default 1). */
131
+ apexGravityScale?: number;
132
+ /** Vertical speed under which the apex gravity scale applies, m/s (default 1.5). */
133
+ apexSpeed?: number;
134
+ /** Gravity multiplier while falling (default 1). Above 1 drops faster than it rose. */
135
+ fallGravityScale?: number;
136
+ /** After landing, jumps wait and walk speed ramps back from {@link MovementTuningOverrides.landingSpeedScale} over this many ms (default 0). */
137
+ landingRecoveryMs?: number;
138
+ /** Walk speed share right after landing when `landingRecoveryMs` is set (default 0.5). */
139
+ landingSpeedScale?: number;
103
140
  }
104
141
  /**
105
142
  * Per-frame dynamic modifiers for {@link advancePlayerMotion} — state the caller
@@ -107,10 +144,16 @@ export interface MovementTuningOverrides {
107
144
  * fields leave the integrator at its default behavior.
108
145
  */
109
146
  export interface MotionFrameOptions {
147
+ /** Integrate a caller-clamped simulation step in full, including scaled game time. Default false. */
148
+ authoritativeStep?: boolean;
110
149
  /** Extra multiplier applied on top of the resolved target speed (e.g. swimming). Default 1. */
111
150
  speedScale?: number;
112
151
  /** Suppress gravity/jump integration and hold the avatar afloat this frame (e.g. swimming). Default false. */
113
152
  floating?: boolean;
153
+ /** Buffers jump presses for `jumpBufferMs`. Without it a jump fires only on the press frame. */
154
+ buffer?: InputBuffer;
155
+ /** A collision controller supplies grounded state; integrate jump velocity without landing on the synthetic offset origin. */
156
+ externalGrounding?: boolean;
114
157
  }
115
158
  /**
116
159
  * Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump,
@@ -124,6 +167,8 @@ export interface MotionFrameOptions {
124
167
  * @internal
125
168
  */
126
169
  export declare function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, tuning?: MovementTuningOverrides, options?: MotionFrameOptions): MovementFrameStep;
170
+ /** @internal Validate trusted simulation time or apply the standalone frame-stall clamp. */
171
+ export declare function motionStepSeconds(seconds: number, authoritativeStep?: boolean): number;
127
172
  /** A placed scene object the walking player collides against as a circle-vs-AABB obstacle. */
128
173
  export interface CollisionObstacle {
129
174
  position: readonly [number, number, number];
@@ -144,6 +189,23 @@ export interface CollisionObstacle {
144
189
  }
145
190
  /** @internal Player radius the walking obstruction inflates obstacles by; callers gather within reach of it. */
146
191
  export declare const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
192
+ /** @internal Default feet-to-head span for heightfield obstruction; matches DEFAULT_VOXEL_DIMS.height. */
193
+ export declare const DEFAULT_OBSTACLE_PLAYER_HEIGHT = 1.8;
194
+ /**
195
+ * Sweep a heightfield walker's vertical step at its accepted XZ position against blocking bounds.
196
+ * The nearest crossed underside stops ascent; the nearest crossed top catches descent, including
197
+ * thin geometry crossed in one step. Compound boxes are relative to obstacle.position; offset is
198
+ * used only for a single AABB. Real starting intersections are reported separately, not repaired.
199
+ * @internal
200
+ */
201
+ export declare function resolveObstacleVerticalStep(position: readonly [number, number, number], stepY: number, obstacles: readonly CollisionObstacle[], radius?: number, playerHeight?: number): {
202
+ stepY: number;
203
+ hitCeiling: boolean;
204
+ ceilingFeetY: number | null;
205
+ ceilingTolerance: number;
206
+ landed: boolean;
207
+ initialOverlap: boolean;
208
+ };
147
209
  /**
148
210
  * Resolve a horizontal step against nearby placed objects with classic
149
211
  * axis-separated sliding (try X, then Z against the post-X position) so
@@ -159,7 +221,7 @@ export declare const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
159
221
  * tread is stepped up, not bumped into.
160
222
  * @internal
161
223
  */
162
- export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number, stepUpHeight?: number): MovementFrameStep;
224
+ export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number, stepUpHeight?: number, playerHeight?: number): MovementFrameStep;
163
225
  /**
164
226
  * Highest blocking-box top the player can stand on at `(x, z)`: among obstacles whose footprint
165
227
  * (inflated by `playerRadius`, matching the blocking inflation so any horizontal contact that would
@@ -1,15 +1,3 @@
1
- /**
2
- * Movement core (pure model).
3
- *
4
- * This module owns the raw key model, the pure translation from held keys
5
- * into a frame-independent {@link MovementIntent}, and the kinematics
6
- * integration. Keeping it framework-free means the kinematics controller,
7
- * the keyboard handler and the on-screen touch D-pad all speak the same
8
- * vocabulary, and the translation is unit testable without a renderer.
9
- * DOM capture lives in `@jgengine/shell`'s GamePlayerShell (keyboard and
10
- * touch controls) and its camera rigs (pointer lock, the per-frame look
11
- * channel).
12
- */
13
1
  /** @internal */
14
2
  export function createEmptyMovementKeys() {
15
3
  return { w: false, a: false, s: false, d: false, shift: false, control: false, c: false, space: false };
@@ -90,9 +78,9 @@ export const MOVEMENT_TUNING = {
90
78
  function resolveTargetSpeed(intent, baseSpeed, tuning) {
91
79
  const backpedal = tuning?.backpedalSpeedMultiplier ?? MOVEMENT_TUNING.backpedalSpeedMultiplier;
92
80
  const speedMultiplier = intent.crouching
93
- ? MOVEMENT_TUNING.crouchSpeedMultiplier
81
+ ? (tuning?.crouchSpeedMultiplier ?? MOVEMENT_TUNING.crouchSpeedMultiplier)
94
82
  : intent.running
95
- ? MOVEMENT_TUNING.runSpeedMultiplier
83
+ ? (tuning?.runSpeedMultiplier ?? MOVEMENT_TUNING.runSpeedMultiplier)
96
84
  : intent.forward < 0
97
85
  ? backpedal
98
86
  : 1;
@@ -107,6 +95,11 @@ export function createPlayerMotionState() {
107
95
  jumpOffset: 0,
108
96
  grounded: true,
109
97
  jumpHeld: false,
98
+ clockMs: 0,
99
+ groundedAtMs: 0,
100
+ landedAtMs: null,
101
+ jumpRising: false,
102
+ wasAirborne: false,
110
103
  };
111
104
  }
112
105
  /**
@@ -121,8 +114,20 @@ export function createPlayerMotionState() {
121
114
  * @internal
122
115
  */
123
116
  export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, tuning, options) {
124
- const deltaSeconds = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
125
- const targetSpeed = resolveTargetSpeed(intent, baseSpeed, tuning) * (options?.speedScale ?? 1);
117
+ const deltaSeconds = motionStepSeconds(rawDeltaSeconds, options?.authoritativeStep);
118
+ const now = (motion.clockMs ?? 0) + deltaSeconds * 1000;
119
+ motion.clockMs = now;
120
+ if (motion.grounded && motion.wasAirborne)
121
+ motion.landedAtMs = now;
122
+ const airborneAtStart = !motion.grounded;
123
+ const landingRecoveryMs = tuning?.landingRecoveryMs ?? 0;
124
+ const landedAtMs = motion.landedAtMs ?? null;
125
+ const recovery = landingRecoveryMs > 0 && motion.grounded && landedAtMs !== null && now - landedAtMs < landingRecoveryMs
126
+ ? (now - landedAtMs) / landingRecoveryMs
127
+ : 1;
128
+ const landingSpeedScale = tuning?.landingSpeedScale ?? 0.5;
129
+ const recoverySpeedScale = landingSpeedScale + (1 - landingSpeedScale) * recovery;
130
+ const targetSpeed = resolveTargetSpeed(intent, baseSpeed, tuning) * (options?.speedScale ?? 1) * recoverySpeedScale;
126
131
  const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
127
132
  const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
128
133
  let targetVelocityX = 0;
@@ -161,48 +166,151 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
161
166
  targetVelocityZ = 0;
162
167
  }
163
168
  }
164
- const acceleration = motion.grounded ? MOVEMENT_TUNING.groundAcceleration : MOVEMENT_TUNING.airAcceleration;
169
+ const acceleration = motion.grounded
170
+ ? (tuning?.groundAcceleration ?? MOVEMENT_TUNING.groundAcceleration)
171
+ : (tuning?.airAcceleration ?? MOVEMENT_TUNING.airAcceleration);
165
172
  const accelerationBlend = 1 - Math.exp(-acceleration * deltaSeconds);
166
173
  motion.horizontalVelocityX += (targetVelocityX - motion.horizontalVelocityX) * accelerationBlend;
167
174
  motion.horizontalVelocityZ += (targetVelocityZ - motion.horizontalVelocityZ) * accelerationBlend;
168
175
  if (!intent.moving && motion.grounded) {
169
- const friction = Math.exp(-MOVEMENT_TUNING.groundFriction * deltaSeconds);
176
+ const friction = Math.exp(-(tuning?.groundFriction ?? MOVEMENT_TUNING.groundFriction) * deltaSeconds);
170
177
  motion.horizontalVelocityX *= friction;
171
178
  motion.horizontalVelocityZ *= friction;
172
179
  }
173
180
  const floating = options?.floating === true;
174
181
  const jumpPressed = intent.jumping;
175
- if (jumpPressed && !motion.jumpHeld && motion.grounded && !intent.crouching && !floating) {
182
+ if (motion.grounded)
183
+ motion.groundedAtMs = now;
184
+ const coyoteMs = tuning?.coyoteMs ?? 0;
185
+ const groundedAtMs = motion.groundedAtMs ?? null;
186
+ const canJump = !intent.crouching &&
187
+ !floating &&
188
+ recovery >= 1 &&
189
+ (motion.grounded ||
190
+ (coyoteMs > 0 && motion.verticalVelocity <= 0 && groundedAtMs !== null && now - groundedAtMs <= coyoteMs));
191
+ const pressEdge = jumpPressed && !motion.jumpHeld;
192
+ const buffer = options?.buffer;
193
+ if (buffer !== undefined) {
194
+ if (pressEdge)
195
+ buffer.press("jump", now);
196
+ else if (!jumpPressed && motion.jumpHeld)
197
+ buffer.release("jump", now);
198
+ }
199
+ const jump = canJump && (buffer === undefined ? pressEdge : buffer.consume("jump", now, tuning?.jumpBufferMs ?? 0));
200
+ if (jump) {
176
201
  motion.verticalVelocity = jumpVelocity;
177
202
  motion.grounded = false;
203
+ motion.groundedAtMs = null;
204
+ motion.jumpRising = true;
205
+ }
206
+ else if (motion.jumpRising && !jumpPressed && motion.jumpHeld && motion.verticalVelocity > 0) {
207
+ motion.verticalVelocity *= Math.max(0, Math.min(1, tuning?.jumpCutFactor ?? 1));
208
+ motion.jumpRising = false;
178
209
  }
179
210
  motion.jumpHeld = jumpPressed;
180
211
  if (floating) {
181
212
  motion.verticalVelocity = 0;
182
213
  motion.jumpOffset = 0;
183
214
  motion.grounded = true;
215
+ motion.jumpRising = false;
184
216
  }
185
217
  else if (!motion.grounded || motion.verticalVelocity > 0) {
186
- motion.verticalVelocity -= gravityAcceleration * deltaSeconds;
218
+ const apexSpeed = tuning?.apexSpeed ?? 1.5;
219
+ const gravityScale = Math.abs(motion.verticalVelocity) < apexSpeed
220
+ ? (tuning?.apexGravityScale ?? 1)
221
+ : motion.verticalVelocity < 0
222
+ ? (tuning?.fallGravityScale ?? 1)
223
+ : 1;
224
+ motion.verticalVelocity -= gravityAcceleration * gravityScale * deltaSeconds;
225
+ if (motion.verticalVelocity <= 0)
226
+ motion.jumpRising = false;
187
227
  motion.jumpOffset += motion.verticalVelocity * deltaSeconds;
188
- if (motion.jumpOffset <= 0) {
228
+ if (motion.jumpOffset <= 0 && options?.externalGrounding !== true) {
189
229
  motion.jumpOffset = 0;
190
230
  motion.verticalVelocity = 0;
191
231
  motion.grounded = true;
192
232
  }
193
233
  }
234
+ if (airborneAtStart && motion.grounded)
235
+ motion.landedAtMs = now;
236
+ motion.wasAirborne = !motion.grounded;
194
237
  return {
195
238
  stepX: motion.horizontalVelocityX * deltaSeconds,
196
239
  stepZ: motion.horizontalVelocityZ * deltaSeconds,
197
240
  };
198
241
  }
242
+ /** @internal Validate trusted simulation time or apply the standalone frame-stall clamp. */
243
+ export function motionStepSeconds(seconds, authoritativeStep = false) {
244
+ if (!Number.isFinite(seconds) || seconds < 0)
245
+ throw new RangeError("Movement step seconds must be finite and nonnegative");
246
+ return authoritativeStep ? seconds : Math.min(seconds, MOVEMENT_TUNING.maxFrameSeconds);
247
+ }
199
248
  const DEFAULT_OBSTACLE_HALF_EXTENTS = [0.5, 0.5, 0.5];
200
249
  /** @internal Player radius the walking obstruction inflates obstacles by; callers gather within reach of it. */
201
250
  export const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
202
- /** Feet-to-head span used for the obstacle's vertical overlap test; matches DEFAULT_VOXEL_DIMS.height. */
203
- const OBSTACLE_PLAYER_HEIGHT = 1.8;
251
+ /** @internal Default feet-to-head span for heightfield obstruction; matches DEFAULT_VOXEL_DIMS.height. */
252
+ export const DEFAULT_OBSTACLE_PLAYER_HEIGHT = 1.8;
204
253
  /** Extra nudge past a solid face when depenetrating so the escaped capsule lands just outside, not on, the box. */
205
254
  const PENETRATION_EPSILON = 1e-3;
255
+ function verticalContactTolerance(feetY, headY, minY, maxY) {
256
+ return Number.EPSILON * 8 * Math.max(1, Math.abs(feetY), Math.abs(headY), Math.abs(minY), Math.abs(maxY));
257
+ }
258
+ /**
259
+ * Sweep a heightfield walker's vertical step at its accepted XZ position against blocking bounds.
260
+ * The nearest crossed underside stops ascent; the nearest crossed top catches descent, including
261
+ * thin geometry crossed in one step. Compound boxes are relative to obstacle.position; offset is
262
+ * used only for a single AABB. Real starting intersections are reported separately, not repaired.
263
+ * @internal
264
+ */
265
+ export function resolveObstacleVerticalStep(position, stepY, obstacles, radius = DEFAULT_OBSTACLE_PLAYER_RADIUS, playerHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT) {
266
+ if (!Number.isFinite(stepY))
267
+ throw new RangeError("Vertical obstacle step must be finite.");
268
+ if (!Number.isFinite(playerHeight) || playerHeight <= 0)
269
+ throw new RangeError("Obstacle player height must be finite and positive.");
270
+ const feetY = position[1];
271
+ const headY = feetY + playerHeight;
272
+ const nextFeet = feetY + stepY;
273
+ const nextHead = nextFeet + playerHeight;
274
+ const result = { stepY, hitCeiling: false, ceilingFeetY: null, ceilingTolerance: 0, landed: false, initialOverlap: false };
275
+ const consider = (minX, maxX, minY, maxY, minZ, maxZ) => {
276
+ if (position[0] <= minX - radius || position[0] >= maxX + radius || position[2] <= minZ - radius || position[2] >= maxZ + radius)
277
+ return;
278
+ const tolerance = verticalContactTolerance(feetY, headY, minY, maxY);
279
+ if (feetY < maxY - tolerance && headY > minY + tolerance) {
280
+ result.initialOverlap = true;
281
+ return;
282
+ }
283
+ if (stepY > 0 && headY <= minY + tolerance && nextHead >= minY) {
284
+ const limitedStep = minY - playerHeight - tolerance - feetY;
285
+ if (limitedStep <= result.stepY) {
286
+ result.stepY = limitedStep;
287
+ // Keep the actual face limit separate from the tiny collision margin, so a support with
288
+ // exactly enough headroom remains valid at translated coordinates.
289
+ result.ceilingFeetY = minY - playerHeight;
290
+ result.ceilingTolerance = tolerance;
291
+ }
292
+ result.hitCeiling = true;
293
+ }
294
+ else if (stepY < 0 && feetY >= maxY - tolerance && nextFeet <= maxY) {
295
+ result.stepY = Math.max(result.stepY, maxY - feetY);
296
+ result.landed = true;
297
+ }
298
+ };
299
+ for (const obstacle of obstacles) {
300
+ const [x, y, z] = obstacle.position;
301
+ if (obstacle.boxes !== undefined && obstacle.boxes.length > 0) {
302
+ for (const box of obstacle.boxes)
303
+ consider(x + box.min[0], x + box.max[0], y + box.min[1], y + box.max[1], z + box.min[2], z + box.max[2]);
304
+ }
305
+ else {
306
+ const half = obstacle.halfExtents ?? DEFAULT_OBSTACLE_HALF_EXTENTS;
307
+ const offset = obstacle.offset;
308
+ const cx = x + (offset?.[0] ?? 0), cy = y + (offset?.[1] ?? 0), cz = z + (offset?.[2] ?? 0);
309
+ consider(cx - half[0], cx + half[0], cy - half[1], cy + half[1], cz - half[2], cz + half[2]);
310
+ }
311
+ }
312
+ return result;
313
+ }
206
314
  /**
207
315
  * Swept clamp of a single-axis step against a box span. Entering from outside stops on the near
208
316
  * face — including when the raw target would land *beyond* the far face (the old form returned the
@@ -242,16 +350,18 @@ function clampAxisStep(originalCoord, step, min, max) {
242
350
  * tread is stepped up, not bumped into.
243
351
  * @internal
244
352
  */
245
- export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0) {
353
+ export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0, playerHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT) {
354
+ if (!Number.isFinite(playerHeight) || playerHeight <= 0)
355
+ throw new RangeError("Obstacle player height must be finite and positive.");
246
356
  const feetY = current[1];
247
- const headY = feetY + OBSTACLE_PLAYER_HEIGHT;
357
+ const headY = feetY + playerHeight;
248
358
  const reachX = Math.abs(stepX) + playerRadius;
249
359
  const reachZ = Math.abs(stepZ) + playerRadius;
250
360
  const nearby = [];
251
361
  const considerBox = (minX, maxX, minY, maxY, minZ, maxZ) => {
252
362
  // Vertical-span cull: a sub-box entirely above the head (walk under the lintel), below the feet,
253
363
  // or low enough to step up onto (`stepUpHeight`) is skipped — walkable ledges don't wall.
254
- if (maxY <= feetY + stepUpHeight || minY >= headY)
364
+ if (maxY <= feetY + stepUpHeight || minY >= headY - verticalContactTolerance(feetY, headY, minY, maxY))
255
365
  return;
256
366
  const centerX = (minX + maxX) / 2;
257
367
  const centerZ = (minZ + maxZ) / 2;
@@ -0,0 +1,51 @@
1
+ import type { PhysicsConfig } from "../game/defineGame.js";
2
+ import type { PlayerMovementConfig } from "../game/playableGame.js";
3
+ /** The walk character {@link measureMovement} drives: the same fields a game passes to `defineGame`. */
4
+ export interface MovementProbeSubject {
5
+ /** `defineGame({ movement })`; `feel` and `backpedalMult` shape the walk. */
6
+ movement?: PlayerMovementConfig;
7
+ /** `defineGame({ physics })`; `gravity` and `jumpVelocity` shape the jump. */
8
+ physics?: PhysicsConfig;
9
+ /** The player entity's `movement.walkSpeed` (default `2`, the controller default). */
10
+ walkSpeed?: number;
11
+ }
12
+ /** Scenario settings for {@link measureMovement}; every field has a default. */
13
+ export interface MovementProbeOptions {
14
+ /** Fixed tick, s (default `1/60`). */
15
+ dt?: number;
16
+ /** Measure with sprint held (default `false`). */
17
+ sprint?: boolean;
18
+ /** How long jump is held for `tapJumpHeight`, s (default `0.1`). */
19
+ tapSeconds?: number;
20
+ }
21
+ /** Deterministic walk-feel metrics. `Infinity` means the target was never reached. */
22
+ export interface MovementReport {
23
+ /** Steady ground speed holding forward, m/s. */
24
+ topSpeed: number;
25
+ /** Standing start until speed reaches 90% of `topSpeed`, s. */
26
+ timeToTopSpeed: number;
27
+ /** Distance slid after releasing input at `topSpeed`, until speed is under 0.05 m/s, m. */
28
+ stopDistance: number;
29
+ /** Seconds from releasing input at `topSpeed` until speed is under 0.05 m/s. */
30
+ stopTime: number;
31
+ /** Seconds from reversing direction at `topSpeed` until speed along the new heading reaches 90% of `topSpeed`. */
32
+ turnAroundTime: number;
33
+ /** Peak height of a held standing jump, m. */
34
+ jumpHeight: number;
35
+ /** Peak height of a standing jump released after `tapSeconds`, m. Lower than `jumpHeight` when `jumpCutFactor` is set. */
36
+ tapJumpHeight: number;
37
+ /** Seconds from takeoff to the jump's peak. */
38
+ apexTime: number;
39
+ /** Seconds from takeoff to landing. */
40
+ airTime: number;
41
+ /** Sideways distance covered by strafing from the first airborne tick of a standing jump until landing, m. */
42
+ airControlReach: number;
43
+ }
44
+ /**
45
+ * Drives a walk character through fixed scenarios (standing start, release to stop, reversal, a standing
46
+ * jump, a tapped jump, a strafed jump) on the same integrator `stepPlayerMovement` uses, and reports the feel metrics a
47
+ * test can assert. Deterministic: the same subject and options always produce the same report, so a feel
48
+ * change shows up as a number moving. Collision, terrain and swimming are out of scope; it measures flat ground.
49
+ * @capability movement-metrics measure a walking character's feel as numbers — time to top speed, stop distance, jump height and apex, air control, turn-around
50
+ */
51
+ export declare function measureMovement(subject?: MovementProbeSubject, options?: MovementProbeOptions): MovementReport;