@jgengine/core 0.19.0 → 0.22.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 (266) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/ai/decisionGraph.d.ts +2 -0
  3. package/dist/ai/decisionGraph.js +6 -0
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/anim/animGraph.d.ts +33 -2
  7. package/dist/anim/animGraph.js +200 -12
  8. package/dist/anim/locomotionGraph.d.ts +11 -3
  9. package/dist/anim/locomotionGraph.js +25 -15
  10. package/dist/audio/audioFalloff.d.ts +4 -0
  11. package/dist/audio/voiceAllocator.d.ts +46 -0
  12. package/dist/audio/voiceAllocator.js +76 -0
  13. package/dist/combat/abilityKit.d.ts +19 -0
  14. package/dist/combat/abilityKit.js +40 -4
  15. package/dist/combat/death.d.ts +4 -3
  16. package/dist/combat/death.js +3 -3
  17. package/dist/combat/magazine.d.ts +9 -0
  18. package/dist/combat/magazine.js +24 -2
  19. package/dist/combat/projectiles.d.ts +94 -2
  20. package/dist/combat/projectiles.js +282 -61
  21. package/dist/combat/regenShield.d.ts +14 -0
  22. package/dist/combat/regenShield.js +23 -7
  23. package/dist/crafting/recipeCatalog.d.ts +16 -0
  24. package/dist/crafting/recipeCatalog.js +55 -0
  25. package/dist/economy/auctionBook.d.ts +1 -1
  26. package/dist/economy/auctionBook.js +11 -1
  27. package/dist/economy/listingBook.d.ts +1 -1
  28. package/dist/economy/listingBook.js +11 -1
  29. package/dist/economy/resourceLedger.js +3 -3
  30. package/dist/economy/shopStock.js +7 -6
  31. package/dist/economy/techTree.js +7 -6
  32. package/dist/economy/unlockPoints.js +3 -6
  33. package/dist/editor/commandApply.js +89 -16
  34. package/dist/editor/commands.d.ts +38 -1
  35. package/dist/editor/commands.js +320 -3
  36. package/dist/editor/creatorStorage.d.ts +59 -0
  37. package/dist/editor/creatorStorage.js +164 -0
  38. package/dist/editor/document.d.ts +4 -0
  39. package/dist/editor/document.js +116 -17
  40. package/dist/editor/index.d.ts +3 -0
  41. package/dist/editor/index.js +1 -0
  42. package/dist/editor/liveSync.d.ts +4 -1
  43. package/dist/editor/liveSync.js +26 -10
  44. package/dist/editor/materialAuthoring.d.ts +14 -0
  45. package/dist/editor/materialAuthoring.js +38 -0
  46. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  47. package/dist/editor/modelNodeVisibility.js +29 -0
  48. package/dist/editor/movementCatalog.d.ts +90 -0
  49. package/dist/editor/movementCatalog.js +144 -0
  50. package/dist/editor/simulation.d.ts +60 -0
  51. package/dist/editor/simulation.js +202 -0
  52. package/dist/editor/staticPrefab.d.ts +17 -0
  53. package/dist/editor/staticPrefab.js +51 -0
  54. package/dist/editor/terrainMaterials.d.ts +10 -0
  55. package/dist/editor/terrainMaterials.js +13 -0
  56. package/dist/editor/types.d.ts +11 -0
  57. package/dist/game/cameraConfig.d.ts +3 -1
  58. package/dist/game/clipRoles.d.ts +2 -1
  59. package/dist/game/clipRoles.js +7 -3
  60. package/dist/game/contentValidation.d.ts +69 -0
  61. package/dist/game/contentValidation.js +118 -0
  62. package/dist/game/controlGate.d.ts +8 -0
  63. package/dist/game/controlGate.js +43 -6
  64. package/dist/game/defineGame.d.ts +32 -3
  65. package/dist/game/dialogueGraph.d.ts +20 -0
  66. package/dist/game/dialogueGraph.js +107 -0
  67. package/dist/game/events.d.ts +5 -0
  68. package/dist/game/keyValueStore.d.ts +17 -4
  69. package/dist/game/keyValueStore.js +35 -23
  70. package/dist/game/lootModifiers.d.ts +17 -0
  71. package/dist/game/lootModifiers.js +26 -0
  72. package/dist/game/narrativeAuthoring.d.ts +67 -0
  73. package/dist/game/narrativeAuthoring.js +122 -0
  74. package/dist/game/playableGame.d.ts +52 -20
  75. package/dist/game/quest.d.ts +15 -0
  76. package/dist/game/quest.js +209 -63
  77. package/dist/game/questCatalog.d.ts +50 -0
  78. package/dist/game/questCatalog.js +205 -0
  79. package/dist/game/saveStore.d.ts +6 -5
  80. package/dist/game/saveStore.js +57 -57
  81. package/dist/game/systemRuntime.d.ts +2 -2
  82. package/dist/game/systemRuntime.js +84 -33
  83. package/dist/gameplay.d.ts +3 -0
  84. package/dist/gameplay.js +3 -0
  85. package/dist/interaction/promptRegistry.d.ts +5 -4
  86. package/dist/interaction/promptRegistry.js +8 -2
  87. package/dist/inventory/inventoryModel.js +9 -6
  88. package/dist/inventory/shapedGrid.d.ts +48 -9
  89. package/dist/inventory/shapedGrid.js +69 -10
  90. package/dist/item/durability.js +4 -0
  91. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  92. package/dist/item/itemInstanceRegistry.js +111 -7
  93. package/dist/item/itemgen.d.ts +1 -1
  94. package/dist/item/itemgen.js +1 -1
  95. package/dist/item/modularItem.js +7 -1
  96. package/dist/material/appearanceSignals.d.ts +53 -0
  97. package/dist/material/appearanceSignals.js +47 -0
  98. package/dist/material/hairCards.d.ts +47 -0
  99. package/dist/material/hairCards.js +136 -0
  100. package/dist/material/materialAsset.d.ts +162 -0
  101. package/dist/material/materialAsset.js +382 -0
  102. package/dist/meta/changelog.d.ts +1 -1
  103. package/dist/meta/changelog.js +348 -1
  104. package/dist/movement/characterController.d.ts +5 -2
  105. package/dist/movement/characterController.js +50 -23
  106. package/dist/movement/freeFlight.d.ts +2 -2
  107. package/dist/movement/freeFlight.js +3 -3
  108. package/dist/movement/movementModel.d.ts +24 -1
  109. package/dist/movement/movementModel.js +74 -7
  110. package/dist/movement/playerMovement.d.ts +28 -1
  111. package/dist/movement/playerMovement.js +425 -102
  112. package/dist/movement/poseState.d.ts +2 -0
  113. package/dist/movement/poseState.js +29 -1
  114. package/dist/movement/solidObstacles.d.ts +3 -1
  115. package/dist/movement/solidObstacles.js +8 -5
  116. package/dist/movement/terrainGrade.d.ts +13 -0
  117. package/dist/movement/terrainGrade.js +33 -0
  118. package/dist/movement/voxelController.d.ts +2 -2
  119. package/dist/movement/voxelController.js +3 -3
  120. package/dist/nav/navGrid.d.ts +19 -1
  121. package/dist/nav/navGrid.js +26 -10
  122. package/dist/nav/solidRoute.d.ts +50 -0
  123. package/dist/nav/solidRoute.js +144 -0
  124. package/dist/physics/ballisticSweep.d.ts +10 -3
  125. package/dist/physics/ballisticSweep.js +53 -29
  126. package/dist/physics/forceVolume.d.ts +41 -0
  127. package/dist/physics/forceVolume.js +88 -0
  128. package/dist/physics/kinematicVehicle.d.ts +8 -3
  129. package/dist/physics/kinematicVehicle.js +2 -2
  130. package/dist/physics/physicsWorldBackend.js +19 -3
  131. package/dist/physics/spatialGrid.d.ts +1 -1
  132. package/dist/physics/spatialGrid.js +8 -9
  133. package/dist/physics/vehicleDynamics.d.ts +3 -2
  134. package/dist/physics/vehicleDynamics.js +2 -2
  135. package/dist/physics/vehicleObstacles.d.ts +3 -2
  136. package/dist/physics/vehicleObstacles.js +29 -5
  137. package/dist/runtime/cameraDirector.d.ts +39 -2
  138. package/dist/runtime/cameraDirector.js +162 -0
  139. package/dist/runtime/commandDispatch.d.ts +14 -0
  140. package/dist/runtime/commandDispatch.js +19 -0
  141. package/dist/runtime/context/combat.d.ts +11 -4
  142. package/dist/runtime/context/combat.js +178 -11
  143. package/dist/runtime/context/deathLoot.d.ts +9 -8
  144. package/dist/runtime/context/deathLoot.js +16 -16
  145. package/dist/runtime/context/scene.d.ts +2 -0
  146. package/dist/runtime/context/scene.js +13 -21
  147. package/dist/runtime/descriptors/baseline.js +34 -3
  148. package/dist/runtime/descriptors/features.d.ts +2 -1
  149. package/dist/runtime/descriptors/features.js +18 -0
  150. package/dist/runtime/gameContext.js +79 -13
  151. package/dist/runtime/gameContextTypes.d.ts +14 -0
  152. package/dist/runtime/headlessRunner.d.ts +3 -1
  153. package/dist/runtime/headlessRunner.js +11 -11
  154. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  155. package/dist/runtime/hostedGameRunner.js +156 -10
  156. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  157. package/dist/runtime/hostedWorldSession.js +154 -16
  158. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  159. package/dist/runtime/inputRecorder.d.ts +1 -1
  160. package/dist/runtime/inputRecorder.js +21 -1
  161. package/dist/runtime/inputSnapshot.d.ts +16 -4
  162. package/dist/runtime/inputSnapshot.js +31 -3
  163. package/dist/runtime/runtimeSave.d.ts +10 -6
  164. package/dist/runtime/runtimeSave.js +11 -5
  165. package/dist/runtime/simContext.d.ts +8 -3
  166. package/dist/runtime/simContext.js +7 -3
  167. package/dist/runtime/simSnapshot.js +2 -0
  168. package/dist/runtime/worldReplication.d.ts +2 -0
  169. package/dist/runtime/worldReplication.js +1 -1
  170. package/dist/scene/assetCatalog.d.ts +2 -0
  171. package/dist/scene/behaviorRuntime.d.ts +26 -5
  172. package/dist/scene/behaviorRuntime.js +263 -16
  173. package/dist/scene/behaviors.d.ts +30 -1
  174. package/dist/scene/behaviors.js +29 -0
  175. package/dist/scene/entityStore.d.ts +14 -3
  176. package/dist/scene/entityStore.js +45 -9
  177. package/dist/scene/mount.d.ts +9 -0
  178. package/dist/scene/mount.js +35 -0
  179. package/dist/scene/possession.d.ts +2 -0
  180. package/dist/scene/possession.js +21 -1
  181. package/dist/scene/sceneRaycast.d.ts +7 -0
  182. package/dist/scene/sceneRaycast.js +55 -0
  183. package/dist/scene/spatial.d.ts +20 -1
  184. package/dist/scene/spatial.js +94 -2
  185. package/dist/scene/vehicleSeat.d.ts +36 -2
  186. package/dist/scene/vehicleSeat.js +89 -2
  187. package/dist/stats/accumulatorMeter.d.ts +10 -0
  188. package/dist/stats/accumulatorMeter.js +6 -0
  189. package/dist/stats/eventMeter.d.ts +9 -2
  190. package/dist/stats/eventMeter.js +5 -2
  191. package/dist/store/defineStore.d.ts +3 -1
  192. package/dist/store/defineStore.js +3 -1
  193. package/dist/store/observableKeyedStore.d.ts +1 -1
  194. package/dist/store/observableKeyedStore.js +8 -6
  195. package/dist/time/simClock.d.ts +6 -2
  196. package/dist/time/simClock.js +5 -0
  197. package/dist/vfx/particleDirector.d.ts +58 -6
  198. package/dist/vfx/particleDirector.js +113 -22
  199. package/dist/vfx/particles.d.ts +68 -5
  200. package/dist/vfx/particles.js +284 -32
  201. package/dist/visibility/assetStreaming.d.ts +21 -3
  202. package/dist/visibility/assetStreaming.js +108 -24
  203. package/dist/visibility/settings.d.ts +8 -2
  204. package/dist/visibility/settings.js +3 -0
  205. package/dist/visibility/visibilitySystem.js +1 -0
  206. package/dist/work/expedition.d.ts +110 -0
  207. package/dist/work/expedition.js +195 -0
  208. package/dist/work/staffedStation.d.ts +29 -0
  209. package/dist/work/staffedStation.js +27 -0
  210. package/dist/world/authoredAnimation.d.ts +38 -0
  211. package/dist/world/authoredAnimation.js +99 -0
  212. package/dist/world/authoredObjects.d.ts +24 -4
  213. package/dist/world/authoredObjects.js +43 -16
  214. package/dist/world/authoredSimulation.d.ts +74 -0
  215. package/dist/world/authoredSimulation.js +302 -0
  216. package/dist/world/authoredWeather.d.ts +120 -0
  217. package/dist/world/authoredWeather.js +314 -0
  218. package/dist/world/buildingKit.d.ts +8 -3
  219. package/dist/world/buildingKit.js +21 -8
  220. package/dist/world/environmentForces.d.ts +21 -0
  221. package/dist/world/environmentForces.js +42 -0
  222. package/dist/world/features.d.ts +3 -3
  223. package/dist/world/footprintGrid.d.ts +55 -0
  224. package/dist/world/footprintGrid.js +201 -0
  225. package/dist/world/grassKind.d.ts +10 -0
  226. package/dist/world/grassKind.js +22 -0
  227. package/dist/world/pathTerrain.d.ts +1 -1
  228. package/dist/world/terraform.d.ts +2 -2
  229. package/dist/world/terraform.js +46 -18
  230. package/dist/world/weather.d.ts +17 -0
  231. package/dist/world/weather.js +116 -20
  232. package/dist/world/worldSolids.d.ts +11 -0
  233. package/dist/world/worldSolids.js +112 -1
  234. package/dist/world.d.ts +5 -5
  235. package/dist/world.js +4 -4
  236. package/package.json +1 -1
  237. package/skills/ce-handoff/SKILL.md +1 -1
  238. package/skills/game-design/SKILL.md +2 -0
  239. package/skills/jgengine/SKILL.md +23 -2
  240. package/skills/jgengine/api.md +28 -17
  241. package/skills/jgengine/recipes/minimal-game.md +7 -2
  242. package/skills/jgengine-combat/SKILL.md +8 -2
  243. package/skills/jgengine-combat/api.md +22 -10
  244. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  245. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  246. package/skills/jgengine-combat/reference.md +24 -2
  247. package/skills/jgengine-gameplay/SKILL.md +63 -0
  248. package/skills/jgengine-gameplay/api.md +131 -37
  249. package/skills/jgengine-gameplay/capabilities.md +68 -0
  250. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  251. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  252. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  253. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  254. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  255. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  256. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  257. package/skills/jgengine-gameplay/reference-systems.md +119 -0
  258. package/skills/jgengine-world/SKILL.md +12 -3
  259. package/skills/jgengine-world/api.md +201 -58
  260. package/skills/jgengine-world/capabilities.md +116 -3
  261. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  262. package/skills/jgengine-world/recipes/controls.md +4 -0
  263. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  264. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  265. package/skills/jgengine-world/reference.md +167 -5
  266. package/skills/jgengine-world/visibility.md +28 -1
@@ -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;
@@ -144,12 +144,16 @@ export interface MovementTuningOverrides {
144
144
  * fields leave the integrator at its default behavior.
145
145
  */
146
146
  export interface MotionFrameOptions {
147
+ /** Integrate a caller-clamped simulation step in full, including scaled game time. Default false. */
148
+ authoritativeStep?: boolean;
147
149
  /** Extra multiplier applied on top of the resolved target speed (e.g. swimming). Default 1. */
148
150
  speedScale?: number;
149
151
  /** Suppress gravity/jump integration and hold the avatar afloat this frame (e.g. swimming). Default false. */
150
152
  floating?: boolean;
151
153
  /** Buffers jump presses for `jumpBufferMs`. Without it a jump fires only on the press frame. */
152
154
  buffer?: InputBuffer;
155
+ /** A collision controller supplies grounded state; integrate jump velocity without landing on the synthetic offset origin. */
156
+ externalGrounding?: boolean;
153
157
  }
154
158
  /**
155
159
  * Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump,
@@ -163,6 +167,8 @@ export interface MotionFrameOptions {
163
167
  * @internal
164
168
  */
165
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;
166
172
  /** A placed scene object the walking player collides against as a circle-vs-AABB obstacle. */
167
173
  export interface CollisionObstacle {
168
174
  position: readonly [number, number, number];
@@ -183,6 +189,23 @@ export interface CollisionObstacle {
183
189
  }
184
190
  /** @internal Player radius the walking obstruction inflates obstacles by; callers gather within reach of it. */
185
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
+ };
186
209
  /**
187
210
  * Resolve a horizontal step against nearby placed objects with classic
188
211
  * axis-separated sliding (try X, then Z against the post-X position) so
@@ -198,7 +221,7 @@ export declare const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
198
221
  * tread is stepped up, not bumped into.
199
222
  * @internal
200
223
  */
201
- 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;
202
225
  /**
203
226
  * Highest blocking-box top the player can stand on at `(x, z)`: among obstacles whose footprint
204
227
  * (inflated by `playerRadius`, matching the blocking inflation so any horizontal contact that would
@@ -114,7 +114,7 @@ export function createPlayerMotionState() {
114
114
  * @internal
115
115
  */
116
116
  export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, tuning, options) {
117
- const deltaSeconds = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
117
+ const deltaSeconds = motionStepSeconds(rawDeltaSeconds, options?.authoritativeStep);
118
118
  const now = (motion.clockMs ?? 0) + deltaSeconds * 1000;
119
119
  motion.clockMs = now;
120
120
  if (motion.grounded && motion.wasAirborne)
@@ -225,7 +225,7 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
225
225
  if (motion.verticalVelocity <= 0)
226
226
  motion.jumpRising = false;
227
227
  motion.jumpOffset += motion.verticalVelocity * deltaSeconds;
228
- if (motion.jumpOffset <= 0) {
228
+ if (motion.jumpOffset <= 0 && options?.externalGrounding !== true) {
229
229
  motion.jumpOffset = 0;
230
230
  motion.verticalVelocity = 0;
231
231
  motion.grounded = true;
@@ -239,13 +239,78 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
239
239
  stepZ: motion.horizontalVelocityZ * deltaSeconds,
240
240
  };
241
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
+ }
242
248
  const DEFAULT_OBSTACLE_HALF_EXTENTS = [0.5, 0.5, 0.5];
243
249
  /** @internal Player radius the walking obstruction inflates obstacles by; callers gather within reach of it. */
244
250
  export const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
245
- /** Feet-to-head span used for the obstacle's vertical overlap test; matches DEFAULT_VOXEL_DIMS.height. */
246
- 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;
247
253
  /** Extra nudge past a solid face when depenetrating so the escaped capsule lands just outside, not on, the box. */
248
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
+ }
249
314
  /**
250
315
  * Swept clamp of a single-axis step against a box span. Entering from outside stops on the near
251
316
  * face — including when the raw target would land *beyond* the far face (the old form returned the
@@ -285,16 +350,18 @@ function clampAxisStep(originalCoord, step, min, max) {
285
350
  * tread is stepped up, not bumped into.
286
351
  * @internal
287
352
  */
288
- 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.");
289
356
  const feetY = current[1];
290
- const headY = feetY + OBSTACLE_PLAYER_HEIGHT;
357
+ const headY = feetY + playerHeight;
291
358
  const reachX = Math.abs(stepX) + playerRadius;
292
359
  const reachZ = Math.abs(stepZ) + playerRadius;
293
360
  const nearby = [];
294
361
  const considerBox = (minX, maxX, minY, maxY, minZ, maxZ) => {
295
362
  // Vertical-span cull: a sub-box entirely above the head (walk under the lintel), below the feet,
296
363
  // or low enough to step up onto (`stepUpHeight`) is skipped — walkable ledges don't wall.
297
- if (maxY <= feetY + stepUpHeight || minY >= headY)
364
+ if (maxY <= feetY + stepUpHeight || minY >= headY - verticalContactTolerance(feetY, headY, minY, maxY))
298
365
  return;
299
366
  const centerX = (minX + maxX) / 2;
300
367
  const centerZ = (minZ + maxZ) / 2;
@@ -12,6 +12,8 @@ import { type FreeFlightState } from "./freeFlight.js";
12
12
  import { type VoxelPlayerBody } from "./voxelController.js";
13
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. */
14
14
  export interface PlayerMovementTuning {
15
+ /** The simulation driver already clamps frame stalls; integrate its scaled step in full. */
16
+ authoritativeStep?: boolean;
15
17
  collision?: VoxelCollisionConfig;
16
18
  movement?: PlayerMovementConfig;
17
19
  physics?: MovementTuningOverrides;
@@ -36,6 +38,27 @@ export declare function resolvePlayerMovementTuning(opts: {
36
38
  physics?: PhysicsConfig;
37
39
  world?: WorldFeature;
38
40
  }): PlayerMovementTuning;
41
+ /** Last completed shared movement step for one entity; a live read-only view, not a save snapshot. */
42
+ export interface PlayerMovementTelemetry {
43
+ grounded: boolean;
44
+ verticalVelocity: number;
45
+ crouching: boolean;
46
+ /** Accepted physical height for heightfield walking; absent for other collision paths. */
47
+ collisionHeight?: number;
48
+ /** Live authored height requested for this completed heightfield step. */
49
+ requestedCollisionHeight?: number;
50
+ /** Growth is waiting for headroom; the accepted physical height remains collisionHeight. */
51
+ collisionHeightBlocked?: boolean;
52
+ }
53
+ /**
54
+ * Read the entity's last shared movement result through indexed possession ownership. Returns `null` before a
55
+ * step, after movement restore/forget, when a commit policy replaces the motor proposal, or for an entity not
56
+ * currently driven by this motor. Reuses one live view;
57
+ * custom movers supply their own animation parameters. Flight never reports ground contact or a walking crouch.
58
+ *
59
+ * @capability movement-telemetry read physical grounded, vertical velocity and crouch state without animation-store writes
60
+ */
61
+ export declare function playerMovementTelemetry(ctx: GameContext, entityId: string): Readonly<PlayerMovementTelemetry> | null;
39
62
  /** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
40
63
  export declare function playerMovementHeading(ctx: GameContext, userId: string): number;
41
64
  /** One player's serializable movement state: heading, facing, velocities, jump latch and controller capsule. The entity pose lives in the entity store. */
@@ -49,10 +72,14 @@ export interface PlayerMovementSnapshot {
49
72
  controllerJumpHeld: boolean;
50
73
  /** Buffered jump presses; absent in snapshots taken before jump buffering existed. */
51
74
  jumpBuffer?: InputBufferSnapshot | null;
75
+ /** Accepted heightfield body height. Older walking snapshots retain their historical 1.8m span. */
76
+ heightfieldHeight?: number | null;
77
+ /** Pawn that accepted heightfieldHeight; omission binds legacy dimensions to the active pawn on restore. */
78
+ heightfieldEntityId?: string | null;
52
79
  }
53
80
  /** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
54
81
  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. */
82
+ /** Put a player's movement state back to a {@link snapshotPlayerMovement} copy. Pending state survives until first spawn; a driven actor's despawn clears it. */
56
83
  export declare function restorePlayerMovement(ctx: GameContext, userId: string, snapshot: PlayerMovementSnapshot): void;
57
84
  /** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
58
85
  export declare function forgetPlayerMovement(ctx: GameContext, userId: string): void;