@jgengine/core 0.18.0 → 0.19.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 (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. package/skills/jgengine-world/visibility.md +112 -0
@@ -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
@@ -111,6 +148,8 @@ export interface MotionFrameOptions {
111
148
  speedScale?: number;
112
149
  /** Suppress gravity/jump integration and hold the avatar afloat this frame (e.g. swimming). Default false. */
113
150
  floating?: boolean;
151
+ /** Buffers jump presses for `jumpBufferMs`. Without it a jump fires only on the press frame. */
152
+ buffer?: InputBuffer;
114
153
  }
115
154
  /**
116
155
  * Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump,
@@ -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
  /**
@@ -122,7 +115,19 @@ export function createPlayerMotionState() {
122
115
  */
123
116
  export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, tuning, options) {
124
117
  const deltaSeconds = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
125
- const targetSpeed = resolveTargetSpeed(intent, baseSpeed, tuning) * (options?.speedScale ?? 1);
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,29 +166,64 @@ 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
228
  if (motion.jumpOffset <= 0) {
189
229
  motion.jumpOffset = 0;
@@ -191,6 +231,9 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
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,
@@ -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;
@@ -0,0 +1,119 @@
1
+ import { createInputBuffer } from "../input/inputBuffer.js";
2
+ import { advancePlayerMotion, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, } from "./movementModel.js";
3
+ import { resolvePlayerMovementTuning } from "./playerMovement.js";
4
+ const ARRIVED = 0.9;
5
+ const STOPPED = 0.05;
6
+ const SETTLE_SECONDS = 5;
7
+ const LIMIT_SECONDS = 10;
8
+ function held(keys) {
9
+ return resolveMovementIntent({ ...createEmptyMovementKeys(), ...keys }, true);
10
+ }
11
+ function speedOf(motion) {
12
+ return Math.hypot(motion.horizontalVelocityX, motion.horizontalVelocityZ);
13
+ }
14
+ /**
15
+ * Drives a walk character through fixed scenarios (standing start, release to stop, reversal, a standing
16
+ * jump, a tapped jump, a strafed jump) on the same integrator `stepPlayerMovement` uses, and reports the feel metrics a
17
+ * test can assert. Deterministic: the same subject and options always produce the same report, so a feel
18
+ * change shows up as a number moving. Collision, terrain and swimming are out of scope; it measures flat ground.
19
+ * @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
20
+ */
21
+ export function measureMovement(subject = {}, options = {}) {
22
+ const dt = options.dt ?? 1 / 60;
23
+ const sprint = options.sprint === true;
24
+ const walkSpeed = subject.walkSpeed ?? 2;
25
+ const tuning = resolvePlayerMovementTuning({
26
+ ...(subject.movement === undefined ? {} : { movement: subject.movement }),
27
+ ...(subject.physics === undefined ? {} : { physics: subject.physics }),
28
+ }).physics;
29
+ const buffer = createInputBuffer({ windowMs: 0 });
30
+ const step = (motion, intent, forwardZ = 1, forwardX = 0) => advancePlayerMotion(motion, intent, forwardX, forwardZ, walkSpeed, dt, tuning, { buffer });
31
+ const forward = held({ w: true, shift: sprint });
32
+ const idle = held({});
33
+ const ticks = (seconds) => Math.ceil(seconds / dt);
34
+ const fresh = () => {
35
+ buffer.restore({ windowMs: 0, actions: {} });
36
+ return createPlayerMotionState();
37
+ };
38
+ const settled = () => {
39
+ const motion = fresh();
40
+ for (let i = 0; i < ticks(SETTLE_SECONDS); i += 1)
41
+ step(motion, forward);
42
+ return motion;
43
+ };
44
+ const topSpeed = speedOf(settled());
45
+ let timeToTopSpeed = Number.POSITIVE_INFINITY;
46
+ {
47
+ const motion = fresh();
48
+ for (let i = 1; i <= ticks(LIMIT_SECONDS); i += 1) {
49
+ step(motion, forward);
50
+ if (speedOf(motion) >= topSpeed * ARRIVED) {
51
+ timeToTopSpeed = i * dt;
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ let stopDistance = Number.POSITIVE_INFINITY;
57
+ let stopTime = Number.POSITIVE_INFINITY;
58
+ {
59
+ const motion = settled();
60
+ let distance = 0;
61
+ for (let i = 1; i <= ticks(LIMIT_SECONDS); i += 1) {
62
+ const moved = step(motion, idle);
63
+ distance += Math.hypot(moved.stepX, moved.stepZ);
64
+ if (speedOf(motion) < STOPPED) {
65
+ stopDistance = distance;
66
+ stopTime = i * dt;
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ let turnAroundTime = Number.POSITIVE_INFINITY;
72
+ {
73
+ const motion = settled();
74
+ for (let i = 1; i <= ticks(LIMIT_SECONDS); i += 1) {
75
+ step(motion, forward, -1);
76
+ if (-motion.horizontalVelocityZ >= topSpeed * ARRIVED) {
77
+ turnAroundTime = i * dt;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ const jump = (airborne, releaseAfter = Number.POSITIVE_INFINITY) => {
83
+ const motion = fresh();
84
+ const takeoff = held({ space: true });
85
+ const released = held({ d: airborne.right !== 0, shift: sprint });
86
+ let height = 0;
87
+ let apex = 0;
88
+ let lateral = 0;
89
+ let landed = Number.POSITIVE_INFINITY;
90
+ for (let i = 1; i <= ticks(LIMIT_SECONDS); i += 1) {
91
+ const moved = step(motion, i === 1 ? takeoff : i * dt > releaseAfter ? released : airborne);
92
+ lateral += moved.stepX;
93
+ if (motion.jumpOffset > height) {
94
+ height = motion.jumpOffset;
95
+ apex = i * dt;
96
+ }
97
+ if (motion.grounded && i > 1) {
98
+ landed = i * dt;
99
+ break;
100
+ }
101
+ }
102
+ return { height, apex, lateral, landed };
103
+ };
104
+ const standing = jump(held({ space: true }));
105
+ const strafed = jump(held({ space: true, d: true, shift: sprint }));
106
+ const tapped = jump(held({ space: true }), options.tapSeconds ?? 0.1);
107
+ return {
108
+ topSpeed,
109
+ timeToTopSpeed,
110
+ stopDistance,
111
+ stopTime,
112
+ turnAroundTime,
113
+ jumpHeight: standing.height,
114
+ tapJumpHeight: tapped.height,
115
+ apexTime: standing.apex,
116
+ airTime: standing.landed,
117
+ airControlReach: Math.abs(strafed.lateral),
118
+ };
119
+ }
@@ -2,14 +2,24 @@ import type { PhysicsConfig } from "../game/defineGame.js";
2
2
  import type { PlayerMovementConfig, VoxelCollisionConfig } from "../game/playableGame.js";
3
3
  import type { GameContext } from "../runtime/gameContext.js";
4
4
  import type { InputFrame } from "../runtime/inputSnapshot.js";
5
+ import { type InputBufferSnapshot } from "../input/inputBuffer.js";
6
+ import { type CharacterControllerConfig, type CharacterControllerState } from "./characterController.js";
5
7
  import { type TerrainField } from "../world/terrain.js";
6
8
  import type { WorldFeature } from "../world/features.js";
7
- import { type MovementTuningOverrides } from "./movementModel.js";
9
+ import type { PhysicsBackend } from "../physics/physicsBackend.js";
10
+ import { type MovementTuningOverrides, type PlayerMotionState } from "./movementModel.js";
11
+ import { type FreeFlightState } from "./freeFlight.js";
12
+ import { type VoxelPlayerBody } from "./voxelController.js";
8
13
  /** The resolved, per-world movement configuration {@link stepPlayerMovement} integrates against — the same inputs the shell FrameDriver used to read piecemeal, gathered into one struct so single-player and host movement run identical math. */
9
14
  export interface PlayerMovementTuning {
10
15
  collision?: VoxelCollisionConfig;
11
16
  movement?: PlayerMovementConfig;
12
17
  physics?: MovementTuningOverrides;
18
+ /** Optional physics-backed capsule controller; when present it replaces terrain/AABB walking collision. */
19
+ controller?: {
20
+ backend: PhysicsBackend;
21
+ capsule: CharacterControllerConfig;
22
+ };
13
23
  ground: TerrainField;
14
24
  hasTerrain: boolean;
15
25
  }
@@ -28,6 +38,22 @@ export declare function resolvePlayerMovementTuning(opts: {
28
38
  }): PlayerMovementTuning;
29
39
  /** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
30
40
  export declare function playerMovementHeading(ctx: GameContext, userId: string): number;
41
+ /** One player's serializable movement state: heading, facing, velocities, jump latch and controller capsule. The entity pose lives in the entity store. */
42
+ export interface PlayerMovementSnapshot {
43
+ heading: number;
44
+ facing: number | null;
45
+ voxelBody: VoxelPlayerBody | null;
46
+ motion: PlayerMotionState | null;
47
+ flight: FreeFlightState | null;
48
+ controller: CharacterControllerState | null;
49
+ controllerJumpHeld: boolean;
50
+ /** Buffered jump presses; absent in snapshots taken before jump buffering existed. */
51
+ jumpBuffer?: InputBufferSnapshot | null;
52
+ }
53
+ /** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
54
+ export declare function snapshotPlayerMovement(ctx: GameContext, userId: string): PlayerMovementSnapshot | null;
55
+ /** Put a player's movement state back to a {@link snapshotPlayerMovement} copy, so the next {@link stepPlayerMovement} replays from there. */
56
+ export declare function restorePlayerMovement(ctx: GameContext, userId: string, snapshot: PlayerMovementSnapshot): void;
31
57
  /** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
32
58
  export declare function forgetPlayerMovement(ctx: GameContext, userId: string): void;
33
59
  /**
@@ -36,6 +62,7 @@ export declare function forgetPlayerMovement(ctx: GameContext, userId: string):
36
62
  * so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain,
37
63
  * scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body
38
64
  * per `userId` on the `ctx`. Pass `heading` to override the internally-integrated yaw (the shell owns yaw for its
39
- * camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions.
65
+ * camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions. Pass `pitch` for
66
+ * 6DOF spectator flight when `alignWithLook` is set.
40
67
  */
41
- export declare function stepPlayerMovement(ctx: GameContext, userId: string, input: InputFrame, dt: number, tuning: PlayerMovementTuning, heading?: number): void;
68
+ export declare function stepPlayerMovement(ctx: GameContext, userId: string, input: InputFrame, dt: number, tuning: PlayerMovementTuning, heading?: number, pitch?: number): void;