@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -0,0 +1,54 @@
1
+ export function createGlideModel(config = {}) {
2
+ const gravity = (config.gravity ?? 20) * (config.gravityScale ?? 0.25);
3
+ const thrustAccel = config.thrustAccel ?? 10;
4
+ const forwardSpeed = config.forwardSpeed ?? 12;
5
+ const yawRate = config.yawRate ?? 1.6;
6
+ const climbAccel = config.climbAccel ?? 0;
7
+ const maxFallSpeed = config.maxFallSpeed ?? 8;
8
+ const baseControl = config.control ?? 1;
9
+ let x = 0;
10
+ let y = 0;
11
+ let z = 0;
12
+ let heading = 0;
13
+ let vx = 0;
14
+ let vy = 0;
15
+ let vz = 0;
16
+ function snapshot() {
17
+ return { position: [x, y, z], velocity: [vx, vy, vz], heading };
18
+ }
19
+ return {
20
+ launch(position, nextHeading, initialSpeed = forwardSpeed) {
21
+ x = position[0];
22
+ y = position[1];
23
+ z = position[2];
24
+ heading = nextHeading;
25
+ vx = Math.sin(heading) * initialSpeed;
26
+ vy = 0;
27
+ vz = Math.cos(heading) * initialSpeed;
28
+ },
29
+ pose: snapshot,
30
+ step(dt, input = {}, externalVelocity) {
31
+ if (dt <= 0)
32
+ return snapshot();
33
+ const control = Math.max(0, Math.min(1, input.control ?? baseControl));
34
+ heading += (input.yaw ?? 0) * yawRate * control * dt;
35
+ const fx = Math.sin(heading);
36
+ const fz = Math.cos(heading);
37
+ const along = vx * fx + vz * fz;
38
+ const cruise = (forwardSpeed - along) * Math.min(1, (thrustAccel / Math.max(1, forwardSpeed)) * dt);
39
+ vx += fx * cruise * control;
40
+ vz += fz * cruise * control;
41
+ vy -= gravity * dt;
42
+ const throttle = Math.max(0, Math.min(1, input.throttle ?? 0));
43
+ if (climbAccel > 0 && throttle > 0)
44
+ vy += climbAccel * throttle * control * dt;
45
+ if (vy < -maxFallSpeed)
46
+ vy = -maxFallSpeed;
47
+ const [ex, ey, ez] = externalVelocity ?? [0, 0, 0];
48
+ x += (vx + ex) * dt;
49
+ y += (vy + ey) * dt;
50
+ z += (vz + ez) * dt;
51
+ return snapshot();
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,34 @@
1
+ export type SwingVec3 = readonly [number, number, number];
2
+ export interface GrappleSwingConfig {
3
+ /** Rope shortens by this many units/s while `reeling`. Default `0` (fixed length). */
4
+ reelSpeed?: number;
5
+ minLength?: number;
6
+ /** Extra spring pull toward the anchor when the rope is taut (elastic feel); default `0` (hard constraint only). */
7
+ stiffness?: number;
8
+ /** Velocity damping per second while attached; default `0.1`. */
9
+ damping?: number;
10
+ }
11
+ export interface GrappleSwingState {
12
+ attached: boolean;
13
+ anchor: SwingVec3 | null;
14
+ ropeLength: number;
15
+ }
16
+ export interface GrappleSwingStep {
17
+ position: SwingVec3;
18
+ velocity: SwingVec3;
19
+ }
20
+ /**
21
+ * A rope-swing constraint for kinematic controllers (#282.5) — the walk-controller-compatible
22
+ * sibling of `physics/traversal`'s rigid-body `Grapple`. Fire at an anchor, feed each frame's
23
+ * integrated position/velocity through `step`, and commit what comes back (via `beforeCommit`
24
+ * or `setPose`): positions beyond the rope clamp to its sphere and velocity loses its outward
25
+ * radial component, which is exactly a pendulum swing.
26
+ */
27
+ export interface GrappleSwing {
28
+ fire(anchor: SwingVec3, from: SwingVec3): void;
29
+ release(): void;
30
+ state(): GrappleSwingState;
31
+ /** Constrain one frame. `reeling` shortens the rope; returns the corrected position/velocity. */
32
+ step(position: SwingVec3, velocity: SwingVec3, dt: number, reeling?: boolean): GrappleSwingStep;
33
+ }
34
+ export declare function createGrappleSwing(config?: GrappleSwingConfig): GrappleSwing;
@@ -0,0 +1,56 @@
1
+ export function createGrappleSwing(config = {}) {
2
+ const reelSpeed = config.reelSpeed ?? 0;
3
+ const minLength = config.minLength ?? 1;
4
+ const stiffness = config.stiffness ?? 0;
5
+ const damping = config.damping ?? 0.1;
6
+ let anchor = null;
7
+ let ropeLength = 0;
8
+ return {
9
+ fire(nextAnchor, from) {
10
+ anchor = nextAnchor;
11
+ ropeLength = Math.max(minLength, Math.hypot(from[0] - nextAnchor[0], from[1] - nextAnchor[1], from[2] - nextAnchor[2]));
12
+ },
13
+ release() {
14
+ anchor = null;
15
+ },
16
+ state: () => ({ attached: anchor !== null, anchor, ropeLength }),
17
+ step(position, velocity, dt, reeling = false) {
18
+ if (anchor === null || dt <= 0)
19
+ return { position, velocity };
20
+ if (reeling && reelSpeed > 0)
21
+ ropeLength = Math.max(minLength, ropeLength - reelSpeed * dt);
22
+ let px = position[0];
23
+ let py = position[1];
24
+ let pz = position[2];
25
+ let vx = velocity[0];
26
+ let vy = velocity[1];
27
+ let vz = velocity[2];
28
+ const dx = px - anchor[0];
29
+ const dy = py - anchor[1];
30
+ const dz = pz - anchor[2];
31
+ const distance = Math.hypot(dx, dy, dz);
32
+ if (distance > ropeLength && distance > 1e-9) {
33
+ const nx = dx / distance;
34
+ const ny = dy / distance;
35
+ const nz = dz / distance;
36
+ px = anchor[0] + nx * ropeLength;
37
+ py = anchor[1] + ny * ropeLength;
38
+ pz = anchor[2] + nz * ropeLength;
39
+ const radial = vx * nx + vy * ny + vz * nz;
40
+ if (radial > 0) {
41
+ vx -= nx * radial;
42
+ vy -= ny * radial;
43
+ vz -= nz * radial;
44
+ }
45
+ if (stiffness > 0) {
46
+ const pull = (distance - ropeLength) * stiffness * dt;
47
+ vx -= nx * pull;
48
+ vy -= ny * pull;
49
+ vz -= nz * pull;
50
+ }
51
+ }
52
+ const damp = Math.max(0, 1 - damping * dt);
53
+ return { position: [px, py, pz], velocity: [vx * damp, vy * damp, vz * damp] };
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,29 @@
1
+ export type TrailPoint = readonly [number, number, number];
2
+ export interface LeaderTrailConfig {
3
+ /** Arc-length gap between consecutive followers. */
4
+ spacing: number;
5
+ /** Followers the trail must be able to serve; history is capped to cover them. Default `16`. */
6
+ maxFollowers?: number;
7
+ /** Minimum leader movement before a new trail sample is recorded; default `spacing / 8`. */
8
+ sampleDistance?: number;
9
+ }
10
+ export interface TrailPose {
11
+ position: TrailPoint;
12
+ /** Yaw (radians) of the trail direction at this point, matching engine `rotationY`. */
13
+ heading: number;
14
+ }
15
+ /**
16
+ * A live breadcrumb trail recorded from a moving leader, with followers placed by arc-length
17
+ * behind the head — conga lines, convoys, duckling chains, snake bodies. Call `record` with the
18
+ * leader's position every tick; read `followerAt(i)` for the pose `spacing * (i + 1)` behind.
19
+ */
20
+ export interface LeaderTrail {
21
+ record(position: TrailPoint): void;
22
+ /** Pose for follower `index` (0 = closest behind the leader); `null` until enough trail exists. */
23
+ followerAt(index: number): TrailPose | null;
24
+ /** Recorded arc length available behind the leader. */
25
+ length(): number;
26
+ /** Drop all history (teleport, respawn) and optionally restart from a position. */
27
+ reset(position?: TrailPoint): void;
28
+ }
29
+ export declare function createLeaderTrail(config: LeaderTrailConfig): LeaderTrail;
@@ -0,0 +1,67 @@
1
+ export function createLeaderTrail(config) {
2
+ if (!(config.spacing > 0))
3
+ throw new Error(`leader trail spacing must be positive, got ${config.spacing}`);
4
+ const spacing = config.spacing;
5
+ const maxFollowers = Math.max(1, config.maxFollowers ?? 16);
6
+ const sampleDistance = config.sampleDistance ?? spacing / 8;
7
+ const capacity = spacing * (maxFollowers + 1);
8
+ let points = [];
9
+ let lengths = [];
10
+ let total = 0;
11
+ function trim() {
12
+ while (total > capacity && lengths.length > 0) {
13
+ const tailSegment = lengths[0];
14
+ if (total - tailSegment < capacity)
15
+ break;
16
+ lengths.shift();
17
+ points.shift();
18
+ total -= tailSegment;
19
+ }
20
+ }
21
+ return {
22
+ record(position) {
23
+ const head = points[points.length - 1];
24
+ if (head === undefined) {
25
+ points.push(position);
26
+ return;
27
+ }
28
+ const step = Math.hypot(position[0] - head[0], position[1] - head[1], position[2] - head[2]);
29
+ if (step < sampleDistance)
30
+ return;
31
+ points.push(position);
32
+ lengths.push(step);
33
+ total += step;
34
+ trim();
35
+ },
36
+ followerAt(index) {
37
+ const behind = spacing * (index + 1);
38
+ if (behind > total)
39
+ return null;
40
+ let remaining = behind;
41
+ for (let i = lengths.length - 1; i >= 0; i -= 1) {
42
+ const segment = lengths[i];
43
+ const to = points[i + 1];
44
+ const from = points[i];
45
+ if (remaining <= segment) {
46
+ const fraction = segment <= 1e-9 ? 0 : remaining / segment;
47
+ return {
48
+ position: [
49
+ to[0] + (from[0] - to[0]) * fraction,
50
+ to[1] + (from[1] - to[1]) * fraction,
51
+ to[2] + (from[2] - to[2]) * fraction,
52
+ ],
53
+ heading: Math.atan2(to[0] - from[0], to[2] - from[2]),
54
+ };
55
+ }
56
+ remaining -= segment;
57
+ }
58
+ return null;
59
+ },
60
+ length: () => total,
61
+ reset(position) {
62
+ points = position === undefined ? [] : [position];
63
+ lengths = [];
64
+ total = 0;
65
+ },
66
+ };
67
+ }
@@ -115,6 +115,10 @@ export interface CollisionObstacle {
115
115
  * objects — this also early-exits per obstacle on horizontal distance.
116
116
  */
117
117
  export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number): MovementFrameStep;
118
+ /** Placed objects within `radius` (XZ) of `center`, as {@link CollisionObstacle}s to pre-filter for {@link resolveObstacleStep}. */
119
+ export declare function nearbyObstacles(objects: readonly {
120
+ position: readonly [number, number, number];
121
+ }[], center: readonly [number, number, number], radius?: number): CollisionObstacle[];
118
122
  /** Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`). */
119
123
  export declare function constrainStepToAxis(stepX: number, stepZ: number, axis: "x" | "z"): MovementFrameStep;
120
124
  /**
@@ -227,6 +227,19 @@ export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadi
227
227
  }
228
228
  return { stepX: resultX, stepZ: resultZ };
229
229
  }
230
+ const OBSTACLE_GATHER_RADIUS = 3;
231
+ /** Placed objects within `radius` (XZ) of `center`, as {@link CollisionObstacle}s to pre-filter for {@link resolveObstacleStep}. */
232
+ export function nearbyObstacles(objects, center, radius = OBSTACLE_GATHER_RADIUS) {
233
+ const radiusSq = radius * radius;
234
+ const result = [];
235
+ for (const object of objects) {
236
+ const dx = object.position[0] - center[0];
237
+ const dz = object.position[2] - center[2];
238
+ if (dx * dx + dz * dz <= radiusSq)
239
+ result.push({ position: object.position });
240
+ }
241
+ return result;
242
+ }
230
243
  /** Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`). */
231
244
  export function constrainStepToAxis(stepX, stepZ, axis) {
232
245
  return axis === "x" ? { stepX, stepZ: 0 } : { stepX: 0, stepZ };
@@ -0,0 +1,41 @@
1
+ import type { PhysicsConfig } from "../game/defineGame.js";
2
+ import type { PlayerMovementConfig, VoxelCollisionConfig } from "../game/playableGame.js";
3
+ import type { GameContext } from "../runtime/gameContext.js";
4
+ import type { InputFrame } from "../runtime/inputSnapshot.js";
5
+ import { type TerrainField } from "../world/terrain.js";
6
+ import type { WorldFeature } from "../world/features.js";
7
+ import { type MovementTuningOverrides } from "./movementModel.js";
8
+ /** 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
+ export interface PlayerMovementTuning {
10
+ collision?: VoxelCollisionConfig;
11
+ movement?: PlayerMovementConfig;
12
+ physics?: MovementTuningOverrides;
13
+ ground: TerrainField;
14
+ hasTerrain: boolean;
15
+ }
16
+ /**
17
+ * Maps a game's declared `physics` onto the movement controllers' tuning. `PhysicsConfig.gravity` is a signed
18
+ * world acceleration (negative points down), but the controllers integrate `velocityY -= gravityAcceleration * dt`
19
+ * and expect a positive downward magnitude — so gravity is negated here to keep down-pointing gravity pulling down.
20
+ */
21
+ export declare function resolvePhysicsTuning(physics: PhysicsConfig | undefined): MovementTuningOverrides | undefined;
22
+ /** Gather a game's collision/movement/physics/world config into a {@link PlayerMovementTuning} — call once per world; both the shell and a host pass the result to {@link stepPlayerMovement}. */
23
+ export declare function resolvePlayerMovementTuning(opts: {
24
+ collision?: VoxelCollisionConfig;
25
+ movement?: PlayerMovementConfig;
26
+ physics?: PhysicsConfig;
27
+ world?: WorldFeature;
28
+ }): PlayerMovementTuning;
29
+ /** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
30
+ export declare function playerMovementHeading(ctx: GameContext, userId: string): number;
31
+ /** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
32
+ export declare function forgetPlayerMovement(ctx: GameContext, userId: string): void;
33
+ /**
34
+ * Integrate one player's movement for a tick from their held-input frame and commit the pose — the single
35
+ * genre-agnostic controller both the shell (its local player) and a host (each connected player in `onTick`) call,
36
+ * so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain,
37
+ * scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body
38
+ * 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.
40
+ */
41
+ export declare function stepPlayerMovement(ctx: GameContext, userId: string, input: InputFrame, dt: number, tuning: PlayerMovementTuning, heading?: number): void;
@@ -0,0 +1,181 @@
1
+ import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
2
+ import { groundFieldFor, hasEnvironmentTerrain } from "../world/terrain.js";
3
+ import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, nearbyObstacles, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "./movementModel.js";
4
+ import { steerYaw } from "./steering.js";
5
+ import { advanceVoxelPlayer, createVoxelPlayerBody, } from "./voxelController.js";
6
+ const DEFAULT_TURN_SPEED = 2.4;
7
+ const DEFAULT_WALK_SPEED = 2;
8
+ /**
9
+ * Maps a game's declared `physics` onto the movement controllers' tuning. `PhysicsConfig.gravity` is a signed
10
+ * world acceleration (negative points down), but the controllers integrate `velocityY -= gravityAcceleration * dt`
11
+ * and expect a positive downward magnitude — so gravity is negated here to keep down-pointing gravity pulling down.
12
+ */
13
+ export function resolvePhysicsTuning(physics) {
14
+ if (physics === undefined)
15
+ return undefined;
16
+ return {
17
+ get gravityAcceleration() {
18
+ return physics.gravity === undefined ? undefined : -physics.gravity;
19
+ },
20
+ get jumpVelocity() {
21
+ return physics.jumpVelocity;
22
+ },
23
+ };
24
+ }
25
+ /** Gather a game's collision/movement/physics/world config into a {@link PlayerMovementTuning} — call once per world; both the shell and a host pass the result to {@link stepPlayerMovement}. */
26
+ export function resolvePlayerMovementTuning(opts) {
27
+ const physics = resolvePhysicsTuning(opts.physics);
28
+ return {
29
+ ...(opts.collision === undefined ? {} : { collision: opts.collision }),
30
+ ...(opts.movement === undefined ? {} : { movement: opts.movement }),
31
+ ...(physics === undefined ? {} : { physics }),
32
+ ground: groundFieldFor(opts.world),
33
+ hasTerrain: hasEnvironmentTerrain(opts.world),
34
+ };
35
+ }
36
+ const stores = new WeakMap();
37
+ function storeFor(ctx) {
38
+ let store = stores.get(ctx);
39
+ if (store === undefined) {
40
+ store = { players: new Map(), solids: { count: -1, set: new Set() } };
41
+ stores.set(ctx, store);
42
+ }
43
+ return store;
44
+ }
45
+ function stateFor(store, userId) {
46
+ let state = store.players.get(userId);
47
+ if (state === undefined) {
48
+ state = { heading: 0, voxelBody: null, motion: null };
49
+ store.players.set(userId, state);
50
+ }
51
+ return state;
52
+ }
53
+ /** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
54
+ export function playerMovementHeading(ctx, userId) {
55
+ return stores.get(ctx)?.players.get(userId)?.heading ?? 0;
56
+ }
57
+ /** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
58
+ export function forgetPlayerMovement(ctx, userId) {
59
+ stores.get(ctx)?.players.delete(userId);
60
+ }
61
+ /**
62
+ * Integrate one player's movement for a tick from their held-input frame and commit the pose — the single
63
+ * genre-agnostic controller both the shell (its local player) and a host (each connected player in `onTick`) call,
64
+ * so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain,
65
+ * scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body
66
+ * per `userId` on the `ctx`. Pass `heading` to override the internally-integrated yaw (the shell owns yaw for its
67
+ * camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions.
68
+ */
69
+ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
70
+ const playerId = ctx.player.possession.active(userId);
71
+ const player = ctx.scene.entity.get(playerId);
72
+ if (player === null)
73
+ return;
74
+ const store = storeFor(ctx);
75
+ const state = stateFor(store, userId);
76
+ const held = new Set(input.held);
77
+ const isDown = (action) => held.has(action);
78
+ if (heading !== undefined) {
79
+ state.heading = heading;
80
+ }
81
+ else {
82
+ const turnInput = (isDown("turnRight") ? 1 : 0) - (isDown("turnLeft") ? 1 : 0);
83
+ if (turnInput !== 0)
84
+ state.heading = steerYaw(state.heading, turnInput, DEFAULT_TURN_SPEED, dt);
85
+ }
86
+ const forwardX = Math.sin(state.heading);
87
+ const forwardZ = Math.cos(state.heading);
88
+ const keys = createEmptyMovementKeys();
89
+ keys.w = isDown("moveForward");
90
+ keys.s = isDown("moveBack");
91
+ keys.a = isDown("moveLeft");
92
+ keys.d = isDown("moveRight");
93
+ keys.shift = isDown("sprint") && (tuning.movement?.canSprint?.(ctx) ?? true);
94
+ keys.space = isDown("jump");
95
+ const intent = resolveMovementIntent(keys, true);
96
+ const motionBatch = ctx.player.motionFor(userId).takePending();
97
+ const walkSpeed = player.movement?.walkSpeed ?? DEFAULT_WALK_SPEED;
98
+ if (tuning.collision?.voxel === true) {
99
+ let body = state.voxelBody;
100
+ if (body === null) {
101
+ body = createVoxelPlayerBody(player.position[0], player.position[1], player.position[2]);
102
+ state.voxelBody = body;
103
+ }
104
+ const objects = ctx.scene.object.list();
105
+ if (store.solids.count !== objects.length) {
106
+ store.solids.set = new Set(objects.map((o) => `${o.position[0]},${o.position[1]},${o.position[2]}`));
107
+ store.solids.count = objects.length;
108
+ }
109
+ const solids = store.solids.set;
110
+ const isSolid = (x, y, z) => solids.has(`${x},${y},${z}`);
111
+ const dims = {
112
+ halfWidth: tuning.collision.halfWidth ?? 0.3,
113
+ height: tuning.collision.height ?? 1.8,
114
+ stepHeight: tuning.collision.stepHeight ?? 0.6,
115
+ };
116
+ body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
117
+ [body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
118
+ advanceVoxelPlayer(body, intent, forwardX, forwardZ, walkSpeed, dt, isSolid, dims, tuning.physics, tuning.hasTerrain ? (x, z) => tuning.ground.sampleHeight(x, z) : undefined);
119
+ if (motionBatch !== null && motionBatch.y !== null)
120
+ body.y = motionBatch.y;
121
+ ctx.scene.entity.setPose(playerId, {
122
+ position: [body.x, body.y, body.z],
123
+ rotationY: intent.moving ? Math.atan2(body.velocityX, body.velocityZ) : player.rotationY,
124
+ dt,
125
+ });
126
+ return;
127
+ }
128
+ let motion = state.motion;
129
+ if (motion === null) {
130
+ motion = createPlayerMotionState();
131
+ state.motion = motion;
132
+ }
133
+ motion.verticalVelocity = applyMotionImpulses(motion.verticalVelocity, motionBatch);
134
+ [motion.horizontalVelocityX, motion.horizontalVelocityZ] = applyHorizontalImpulses(motion.horizontalVelocityX, motion.horizontalVelocityZ, motionBatch);
135
+ const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, walkSpeed, dt, tuning.physics);
136
+ let stepX = step.stepX;
137
+ let stepZ = step.stepZ;
138
+ if (tuning.movement?.mode === "axis") {
139
+ const constrained = constrainStepToAxis(stepX, stepZ, tuning.movement.axis ?? "x");
140
+ stepX = constrained.stepX;
141
+ stepZ = constrained.stepZ;
142
+ }
143
+ if (tuning.movement?.collideObjects === true) {
144
+ const obstacles = nearbyObstacles(ctx.scene.object.list(), player.position);
145
+ const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles);
146
+ stepX = resolved.stepX;
147
+ stepZ = resolved.stepZ;
148
+ }
149
+ let nextX = player.position[0] + stepX;
150
+ let nextZ = player.position[2] + stepZ;
151
+ if (tuning.movement?.mode === "grid") {
152
+ const snapped = snapPositionToGrid(nextX, nextZ, tuning.movement.cellSize ?? 1);
153
+ nextX = snapped[0];
154
+ nextZ = snapped[1];
155
+ }
156
+ let nextY = tuning.ground.sampleHeight(nextX, nextZ) + motion.jumpOffset;
157
+ if (motionBatch !== null && motionBatch.y !== null) {
158
+ nextY = motionBatch.y;
159
+ motion.jumpOffset = motionBatch.y - tuning.ground.sampleHeight(nextX, nextZ);
160
+ }
161
+ if (tuning.movement?.beforeCommit !== undefined) {
162
+ const frame = {
163
+ entityId: playerId,
164
+ current: player.position,
165
+ next: [nextX, nextY, nextZ],
166
+ dt,
167
+ ctx,
168
+ };
169
+ const replacement = tuning.movement.beforeCommit(frame);
170
+ if (replacement !== undefined) {
171
+ nextX = replacement[0];
172
+ nextY = replacement[1];
173
+ nextZ = replacement[2];
174
+ }
175
+ }
176
+ ctx.scene.entity.setPose(playerId, {
177
+ position: [nextX, nextY, nextZ],
178
+ rotationY: intent.moving ? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ) : player.rotationY,
179
+ dt,
180
+ });
181
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Yaw-frame steering math. The engine's heading convention is
3
+ * `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a
4
+ * positive yaw increment rotate counterclockwise seen from above — a turn to
5
+ * the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt`
6
+ * therefore turns the wrong way; integrate steering through `steerYaw` instead.
7
+ */
8
+ export type YawVectorXZ = readonly [number, number];
9
+ /** XZ forward direction of a yaw (`rotationY`): `(sin yaw, cos yaw)`. */
10
+ export declare function yawForward(yaw: number): YawVectorXZ;
11
+ /** XZ screen-right of a yaw — `forward × up` with up = +Y: `(-cos yaw, sin yaw)`. */
12
+ export declare function yawRight(yaw: number): YawVectorXZ;
13
+ /**
14
+ * Integrate one steering step. `steerRight` is the signed steer input
15
+ * (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight),
16
+ * `turnRatePerSecond` is radians per second at full lock. Steering right
17
+ * decreases yaw in the engine frame; this helper owns that sign so game
18
+ * code never re-derives it.
19
+ */
20
+ export declare function steerYaw(yaw: number, steerRight: number, turnRatePerSecond: number, dt: number): number;
21
+ /**
22
+ * Signed steer-right input that closes on `desiredYaw` when integrated
23
+ * through `steerYaw` — the seek half every AI driver and homing script
24
+ * needs. Magnitude is the wrapped radian error in (−π, π]; scale and clamp
25
+ * it into [-1, 1] at the call site.
26
+ */
27
+ export declare function steerToward(yaw: number, desiredYaw: number): number;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Yaw-frame steering math. The engine's heading convention is
3
+ * `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a
4
+ * positive yaw increment rotate counterclockwise seen from above — a turn to
5
+ * the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt`
6
+ * therefore turns the wrong way; integrate steering through `steerYaw` instead.
7
+ */
8
+ /** XZ forward direction of a yaw (`rotationY`): `(sin yaw, cos yaw)`. */
9
+ export function yawForward(yaw) {
10
+ return [Math.sin(yaw), Math.cos(yaw)];
11
+ }
12
+ /** XZ screen-right of a yaw — `forward × up` with up = +Y: `(-cos yaw, sin yaw)`. */
13
+ export function yawRight(yaw) {
14
+ return [-Math.cos(yaw), Math.sin(yaw)];
15
+ }
16
+ /**
17
+ * Integrate one steering step. `steerRight` is the signed steer input
18
+ * (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight),
19
+ * `turnRatePerSecond` is radians per second at full lock. Steering right
20
+ * decreases yaw in the engine frame; this helper owns that sign so game
21
+ * code never re-derives it.
22
+ */
23
+ export function steerYaw(yaw, steerRight, turnRatePerSecond, dt) {
24
+ return yaw - steerRight * turnRatePerSecond * dt;
25
+ }
26
+ /**
27
+ * Signed steer-right input that closes on `desiredYaw` when integrated
28
+ * through `steerYaw` — the seek half every AI driver and homing script
29
+ * needs. Magnitude is the wrapped radian error in (−π, π]; scale and clamp
30
+ * it into [-1, 1] at the call site.
31
+ */
32
+ export function steerToward(yaw, desiredYaw) {
33
+ let delta = (desiredYaw - yaw) % TWO_PI;
34
+ if (delta > Math.PI)
35
+ delta -= TWO_PI;
36
+ if (delta <= -Math.PI)
37
+ delta += TWO_PI;
38
+ return -delta;
39
+ }
40
+ const TWO_PI = Math.PI * 2;
@@ -0,0 +1,10 @@
1
+ export type FeedWriteGate = {
2
+ allowedActions: readonly string[];
3
+ };
4
+ export declare function createFeedWriteGate(allowedActions?: readonly string[]): FeedWriteGate;
5
+ export declare function validateFeedWrite(gate: FeedWriteGate | undefined, action: string): {
6
+ ok: true;
7
+ } | {
8
+ ok: false;
9
+ reason: string;
10
+ };
@@ -0,0 +1,12 @@
1
+ export function createFeedWriteGate(allowedActions = []) {
2
+ return { allowedActions: [...allowedActions] };
3
+ }
4
+ export function validateFeedWrite(gate, action) {
5
+ if (gate === undefined || gate.allowedActions.length === 0) {
6
+ return { ok: false, reason: "client feed writes are disabled" };
7
+ }
8
+ if (!gate.allowedActions.includes(action)) {
9
+ return { ok: false, reason: `feed action not allowed: ${action}` };
10
+ }
11
+ return { ok: true };
12
+ }
@@ -22,8 +22,10 @@ export interface IncomingPose {
22
22
  export interface PoseSyncRules {
23
23
  /** Speed cap (units/sec) for client-authoritative movement. */
24
24
  maxSpeed: number;
25
- /** Vertical offset clamp (e.g. peak jump height). */
25
+ /** Vertical offset clamp above floorY (e.g. peak jump height). */
26
26
  maxVerticalOffset: number;
27
+ /** World-floor Y used as the base of the jump band. Defaults to 0. */
28
+ floorY?: number;
27
29
  /** Elapsed-time clamp so stale or bursty clients cannot teleport. */
28
30
  minElapsedSec: number;
29
31
  maxElapsedSec: number;
@@ -44,7 +46,17 @@ export interface PoseSyncDecision {
44
46
  /** True when only the keep-alive stamp should be written. */
45
47
  refreshKeepAlive: boolean;
46
48
  }
47
- export declare function decidePoseSync(current: PresencePoseState, incoming: IncomingPose, rules: PoseSyncRules, nowMs: number): PoseSyncDecision;
49
+ export declare const DEFAULT_SPAWN_ORIGIN: {
50
+ readonly x: 0;
51
+ readonly y: 0;
52
+ readonly z: 0;
53
+ };
54
+ export declare function spawnPresenceState(origin: {
55
+ x: number;
56
+ y: number;
57
+ z: number;
58
+ } | undefined, nowMs: number, rules: Pick<PoseSyncRules, "maxElapsedSec">): PresencePoseState;
59
+ export declare function decidePoseSync(current: PresencePoseState, incoming: IncomingPose, rules: PoseSyncRules, nowMs: number, floorY?: number): PoseSyncDecision;
48
60
  export declare function shouldRefreshKeepAlive(lastSeenAtMs: number | undefined, nowMs: number, rules: Pick<PoseSyncRules, "keepAliveRefreshMs">): boolean;
49
61
  export declare function shouldPersistWorldSnapshot(lastSavedAtMs: number | undefined, nowMs: number, intervalMs: number): boolean;
50
62
  export interface ActivePresenceResolution<T> {
@@ -9,8 +9,18 @@ function appearanceEqual(a, b) {
9
9
  return false;
10
10
  return aKeys.every((key) => a[key] === b[key]);
11
11
  }
12
- export function decidePoseSync(current, incoming, rules, nowMs) {
13
- const elapsedSec = Math.max(rules.minElapsedSec, Math.min(rules.maxElapsedSec, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000));
12
+ export const DEFAULT_SPAWN_ORIGIN = { x: 0, y: 0, z: 0 };
13
+ export function spawnPresenceState(origin = DEFAULT_SPAWN_ORIGIN, nowMs, rules) {
14
+ return {
15
+ position: { x: origin.x, y: origin.y, z: origin.z },
16
+ rotationY: 0,
17
+ rotationPitch: 0,
18
+ lastSeenAtMs: nowMs - rules.maxElapsedSec * 1000,
19
+ };
20
+ }
21
+ export function decidePoseSync(current, incoming, rules, nowMs, floorY) {
22
+ const rawElapsedSec = Math.max(0, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000);
23
+ const elapsedSec = Math.min(rules.maxElapsedSec, rawElapsedSec);
14
24
  const maxDist = rules.maxSpeed * elapsedSec;
15
25
  const maxDistSq = maxDist * maxDist;
16
26
  let targetX = incoming.position.x;
@@ -23,9 +33,11 @@ export function decidePoseSync(current, incoming, rules, nowMs) {
23
33
  targetX = current.position.x + dx * scale;
24
34
  targetZ = current.position.z + dz * scale;
25
35
  }
36
+ const floor = floorY ?? rules.floorY ?? 0;
37
+ const ceiling = floor + rules.maxVerticalOffset;
26
38
  const nextY = incoming.position.y === undefined
27
39
  ? current.position.y
28
- : Math.max(0, Math.min(rules.maxVerticalOffset, incoming.position.y));
40
+ : Math.max(floor, Math.min(ceiling, incoming.position.y));
29
41
  const nextRotationY = incoming.rotationY ?? current.rotationY;
30
42
  const nextRotationPitch = incoming.rotationPitch ?? current.rotationPitch ?? 0;
31
43
  const nextAppearance = incoming.appearance ?? current.appearance;