@jgengine/core 0.17.0 → 0.18.1

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 (174) hide show
  1. package/CHANGELOG.md +130 -2
  2. package/dist/ai/decisionGraph.d.ts +52 -0
  3. package/dist/ai/decisionGraph.js +82 -0
  4. package/dist/ai/mobBrainGraph.d.ts +3 -0
  5. package/dist/ai/mobBrainGraph.js +25 -0
  6. package/dist/ai/tacticalQueries.d.ts +30 -0
  7. package/dist/ai/tacticalQueries.js +45 -0
  8. package/dist/anim/animGraph.d.ts +144 -0
  9. package/dist/anim/animGraph.js +286 -0
  10. package/dist/anim/ikSolver.d.ts +51 -0
  11. package/dist/anim/ikSolver.js +124 -0
  12. package/dist/anim/locomotionGraph.d.ts +26 -0
  13. package/dist/anim/locomotionGraph.js +33 -0
  14. package/dist/assets/spriteAtlas.d.ts +21 -0
  15. package/dist/assets/spriteAtlas.js +103 -0
  16. package/dist/audio/audioFalloff.d.ts +10 -0
  17. package/dist/economy/currency.d.ts +16 -2
  18. package/dist/economy/currency.js +55 -14
  19. package/dist/economy/wallet.d.ts +5 -4
  20. package/dist/economy/wallet.js +8 -4
  21. package/dist/editor/commandApply.js +1 -0
  22. package/dist/editor/commands.d.ts +4 -1
  23. package/dist/editor/document.d.ts +2 -1
  24. package/dist/editor/document.js +89 -0
  25. package/dist/editor/environment.d.ts +3 -0
  26. package/dist/editor/environment.js +16 -0
  27. package/dist/editor/index.d.ts +4 -2
  28. package/dist/editor/index.js +1 -1
  29. package/dist/editor/types.d.ts +16 -0
  30. package/dist/game/cameraConfig.d.ts +8 -0
  31. package/dist/game/chat.d.ts +22 -1
  32. package/dist/game/chat.js +48 -19
  33. package/dist/game/controlGate.d.ts +6 -0
  34. package/dist/game/controlGate.js +30 -1
  35. package/dist/game/defineGame.d.ts +10 -1
  36. package/dist/game/events.d.ts +8 -1
  37. package/dist/game/pixelPerfect.d.ts +25 -0
  38. package/dist/game/pixelPerfect.js +29 -0
  39. package/dist/game/playableGame.d.ts +97 -3
  40. package/dist/game/systemRuntime.js +6 -1
  41. package/dist/input/actionBindings.d.ts +2 -1
  42. package/dist/input/actionBindings.js +10 -8
  43. package/dist/input/actionContexts.d.ts +21 -0
  44. package/dist/input/actionContexts.js +56 -0
  45. package/dist/input/gamepadModel.d.ts +45 -0
  46. package/dist/input/gamepadModel.js +69 -0
  47. package/dist/meta/changelog.d.ts +1 -1
  48. package/dist/meta/changelog.js +103 -1
  49. package/dist/movement/characterController.d.ts +76 -0
  50. package/dist/movement/characterController.js +216 -0
  51. package/dist/movement/freeFlight.d.ts +119 -0
  52. package/dist/movement/freeFlight.js +294 -0
  53. package/dist/movement/playerMovement.d.ts +10 -2
  54. package/dist/movement/playerMovement.js +218 -4
  55. package/dist/movement/serverStep.d.ts +5 -0
  56. package/dist/movement/serverStep.js +6 -0
  57. package/dist/movement/solidObstacles.d.ts +30 -7
  58. package/dist/movement/solidObstacles.js +27 -4
  59. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  60. package/dist/multiplayer/poseSyncGate.js +9 -1
  61. package/dist/multiplayer/presenceContract.d.ts +6 -0
  62. package/dist/multiplayer/presenceModel.d.ts +2 -0
  63. package/dist/multiplayer/presenceModel.js +11 -0
  64. package/dist/nav/navMesh.d.ts +33 -0
  65. package/dist/nav/navMesh.js +164 -0
  66. package/dist/physics/flightDynamics.d.ts +8 -0
  67. package/dist/physics/kinematicVehicle.d.ts +6 -1
  68. package/dist/physics/physicsBackend.d.ts +191 -0
  69. package/dist/physics/physicsBackend.js +49 -0
  70. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  71. package/dist/physics/physicsBackendConformance.js +117 -0
  72. package/dist/physics/physicsWorldBackend.d.ts +44 -0
  73. package/dist/physics/physicsWorldBackend.js +485 -0
  74. package/dist/physics/worldColliders.d.ts +13 -0
  75. package/dist/physics/worldColliders.js +96 -0
  76. package/dist/random/rng.d.ts +12 -1
  77. package/dist/random/rng.js +20 -3
  78. package/dist/render/environment.d.ts +23 -0
  79. package/dist/render/environment.js +1 -0
  80. package/dist/render/lookPreset.d.ts +11 -5
  81. package/dist/render/lookPreset.js +29 -4
  82. package/dist/render/postProcessing.d.ts +10 -1
  83. package/dist/render/sprite2d.d.ts +38 -0
  84. package/dist/render/sprite2d.js +40 -0
  85. package/dist/runtime/adapter.d.ts +20 -1
  86. package/dist/runtime/adapter.js +8 -1
  87. package/dist/runtime/commandInput.d.ts +15 -0
  88. package/dist/runtime/commandInput.js +13 -0
  89. package/dist/runtime/commandRunner.d.ts +44 -3
  90. package/dist/runtime/commandRunner.js +41 -3
  91. package/dist/runtime/gameContext.js +11 -1
  92. package/dist/runtime/gameContextTypes.d.ts +14 -4
  93. package/dist/runtime/gameRuntime.d.ts +33 -4
  94. package/dist/runtime/gameRuntime.js +26 -5
  95. package/dist/runtime/headlessRunner.d.ts +10 -0
  96. package/dist/runtime/headlessRunner.js +26 -8
  97. package/dist/runtime/hostPolicy.d.ts +3 -2
  98. package/dist/runtime/hostPolicy.js +8 -10
  99. package/dist/runtime/hostedGameRunner.js +34 -6
  100. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  101. package/dist/runtime/hostedWorldSession.js +29 -6
  102. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  103. package/dist/runtime/hostedWorldStore.js +1 -0
  104. package/dist/runtime/inputRecorder.d.ts +30 -0
  105. package/dist/runtime/inputRecorder.js +59 -0
  106. package/dist/runtime/inputSnapshot.d.ts +8 -0
  107. package/dist/runtime/inputSnapshot.js +1 -0
  108. package/dist/runtime/poseInterpolation.d.ts +36 -0
  109. package/dist/runtime/poseInterpolation.js +91 -0
  110. package/dist/runtime/prediction.d.ts +45 -0
  111. package/dist/runtime/prediction.js +62 -0
  112. package/dist/runtime/simContext.d.ts +45 -0
  113. package/dist/runtime/simContext.js +65 -0
  114. package/dist/runtime/simLoop.d.ts +52 -0
  115. package/dist/runtime/simLoop.js +72 -0
  116. package/dist/runtime/simSnapshot.d.ts +32 -0
  117. package/dist/runtime/simSnapshot.js +60 -0
  118. package/dist/runtime/snapshot.d.ts +8 -0
  119. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  120. package/dist/runtime/snapshotBuffer.js +58 -0
  121. package/dist/runtime/transport.d.ts +38 -2
  122. package/dist/runtime/worldChunks.d.ts +2 -0
  123. package/dist/runtime/worldChunks.js +14 -0
  124. package/dist/runtime/worldProjection.d.ts +9 -0
  125. package/dist/runtime/worldProjection.js +17 -1
  126. package/dist/runtime/worldSnapshot.d.ts +6 -0
  127. package/dist/runtime/worldSnapshot.js +3 -0
  128. package/dist/scene/behaviorRuntime.d.ts +19 -1
  129. package/dist/scene/behaviorRuntime.js +55 -4
  130. package/dist/scene/behaviors.d.ts +8 -1
  131. package/dist/scene/builtinSceneKinds.js +10 -0
  132. package/dist/sensor/perception.d.ts +60 -0
  133. package/dist/sensor/perception.js +142 -0
  134. package/dist/settings/graphicsProfile.d.ts +19 -0
  135. package/dist/settings/graphicsProfile.js +36 -0
  136. package/dist/time/accrueSince.d.ts +10 -0
  137. package/dist/time/accrueSince.js +9 -0
  138. package/dist/time/rateWindow.d.ts +16 -0
  139. package/dist/time/rateWindow.js +14 -0
  140. package/dist/time/serverTick.d.ts +21 -3
  141. package/dist/time/serverTick.js +14 -13
  142. package/dist/vfx/particleDirector.d.ts +68 -0
  143. package/dist/vfx/particleDirector.js +81 -0
  144. package/dist/world/buildings.d.ts +12 -0
  145. package/dist/world/buildings.js +43 -3
  146. package/dist/world/cityContent.js +103 -25
  147. package/dist/world/cityKind.d.ts +132 -0
  148. package/dist/world/cityKind.js +315 -15
  149. package/dist/world/features.d.ts +27 -1
  150. package/dist/world/features.js +7 -2
  151. package/dist/world/placement.d.ts +7 -0
  152. package/dist/world/placement.js +2 -0
  153. package/dist/world/placementController.d.ts +1 -1
  154. package/dist/world/placementController.js +7 -0
  155. package/dist/world/raceCircuit.d.ts +168 -0
  156. package/dist/world/raceCircuit.js +797 -0
  157. package/dist/world/terrain.d.ts +5 -1
  158. package/dist/world/terrain.js +5 -0
  159. package/dist/world/territory.d.ts +97 -0
  160. package/dist/world/territory.js +182 -0
  161. package/dist/world.d.ts +5 -3
  162. package/dist/world.js +4 -2
  163. package/package.json +1 -1
  164. package/skills/ce-handoff/SKILL.md +1 -1
  165. package/skills/game-design/SKILL.md +11 -1
  166. package/skills/jgengine/SKILL.md +2 -0
  167. package/skills/jgengine/api.md +98 -22
  168. package/skills/jgengine/capabilities.md +28 -0
  169. package/skills/jgengine-gameplay/SKILL.md +10 -0
  170. package/skills/jgengine-gameplay/api.md +40 -18
  171. package/skills/jgengine-gameplay/capabilities.md +12 -0
  172. package/skills/jgengine-world/SKILL.md +6 -0
  173. package/skills/jgengine-world/api.md +312 -12
  174. package/skills/jgengine-world/capabilities.md +102 -0
@@ -1,6 +1,8 @@
1
1
  import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
2
+ import { createCharacterController } from "./characterController.js";
2
3
  import { groundFieldFor, hasEnvironmentTerrain, sampleSlope } from "../world/terrain.js";
3
- import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "./movementModel.js";
4
+ import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, MOVEMENT_TUNING, } from "./movementModel.js";
5
+ import { advanceFreeFlight, createFreeFlightState, resolveFlightStep, resolveFreeFlightIntentFromInput, } from "./freeFlight.js";
4
6
  import { solidObstaclesNear } from "./solidObstacles.js";
5
7
  import { approachYaw, steerYaw } from "./steering.js";
6
8
  import { advanceVoxelPlayer, createVoxelPlayerBody, } from "./voxelController.js";
@@ -39,6 +41,18 @@ export function resolvePlayerMovementTuning(opts) {
39
41
  ...(opts.collision === undefined ? {} : { collision: opts.collision }),
40
42
  ...(opts.movement === undefined ? {} : { movement: opts.movement }),
41
43
  ...(overrides === undefined ? {} : { physics: overrides }),
44
+ ...(opts.physics?.backend === undefined
45
+ ? {}
46
+ : {
47
+ controller: {
48
+ backend: opts.physics.backend,
49
+ capsule: opts.physics.controller ?? {
50
+ radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
51
+ height: 1.8,
52
+ stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
53
+ },
54
+ },
55
+ }),
42
56
  ground: groundFieldFor(opts.world),
43
57
  hasTerrain: hasEnvironmentTerrain(opts.world),
44
58
  };
@@ -58,7 +72,7 @@ function storeFor(ctx) {
58
72
  function stateFor(store, userId) {
59
73
  let state = store.players.get(userId);
60
74
  if (state === undefined) {
61
- state = { heading: 0, facing: null, voxelBody: null, motion: null };
75
+ state = { heading: 0, facing: null, voxelBody: null, motion: null, flight: null, controller: null, controllerJumpHeld: false };
62
76
  store.players.set(userId, state);
63
77
  }
64
78
  return state;
@@ -90,15 +104,41 @@ function resolveBodyFacing(state, moving, velocityX, velocityZ, fallbackYaw, tur
90
104
  state.facing = next;
91
105
  return next;
92
106
  }
107
+ function flightTuningFor(movement, ctx) {
108
+ const cfg = movement?.flight;
109
+ if (cfg === undefined || cfg === false)
110
+ return null;
111
+ if (cfg === true)
112
+ return { mode: "creative" };
113
+ if (typeof cfg === "object") {
114
+ if (cfg.canFly !== undefined && !cfg.canFly(ctx))
115
+ return null;
116
+ return {
117
+ mode: cfg.mode ?? "creative",
118
+ speed: cfg.speed,
119
+ verticalSpeed: cfg.verticalSpeed,
120
+ sprintMultiplier: cfg.sprintMultiplier,
121
+ acceleration: cfg.acceleration,
122
+ gravity: cfg.gravity,
123
+ thrust: cfg.thrust,
124
+ alignWithLook: cfg.alignWithLook,
125
+ collide: cfg.collide,
126
+ bindings: cfg.bindings,
127
+ camera: cfg.camera ?? null,
128
+ };
129
+ }
130
+ return null;
131
+ }
93
132
  /**
94
133
  * Integrate one player's movement for a tick from their held-input frame and commit the pose — the single
95
134
  * genre-agnostic controller both the shell (its local player) and a host (each connected player in `onTick`) call,
96
135
  * so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain,
97
136
  * scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body
98
137
  * per `userId` on the `ctx`. Pass `heading` to override the internally-integrated yaw (the shell owns yaw for its
99
- * camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions.
138
+ * camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions. Pass `pitch` for
139
+ * 6DOF spectator flight when `alignWithLook` is set.
100
140
  */
101
- export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
141
+ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitch) {
102
142
  const playerId = ctx.player.possession.active(userId);
103
143
  const player = ctx.scene.entity.get(playerId);
104
144
  if (player === null)
@@ -141,6 +181,180 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
141
181
  const intent = resolveMovementIntent(keys, true, analogMove);
142
182
  const motionBatch = ctx.player.motionFor(userId).takePending();
143
183
  const walkSpeed = player.movement?.walkSpeed ?? DEFAULT_WALK_SPEED;
184
+ const flightTuning = flightTuningFor(tuning.movement, ctx);
185
+ if (flightTuning !== null) {
186
+ const value = (action) => input.analog?.[action] ?? (isDown(action) ? 1 : 0);
187
+ let flightIntent = resolveFreeFlightIntentFromInput(isDown, value, input.pointer, flightTuning.bindings);
188
+ if (tuning.movement?.canSprint !== undefined && !tuning.movement.canSprint(ctx)) {
189
+ flightIntent = { ...flightIntent, sprint: false };
190
+ }
191
+ let flightState = state.flight;
192
+ if (flightState === null) {
193
+ flightState = createFreeFlightState();
194
+ state.flight = flightState;
195
+ }
196
+ if (motionBatch !== null) {
197
+ flightState.vy = applyMotionImpulses(flightState.vy, motionBatch);
198
+ const [hx, hz] = applyHorizontalImpulses(flightState.vx, flightState.vz, motionBatch);
199
+ flightState.vx = hx;
200
+ flightState.vz = hz;
201
+ }
202
+ const normalizedFlightTuning = {
203
+ mode: flightTuning.mode,
204
+ speed: flightTuning.speed,
205
+ verticalSpeed: flightTuning.verticalSpeed,
206
+ sprintMultiplier: flightTuning.sprintMultiplier,
207
+ acceleration: flightTuning.acceleration,
208
+ gravity: flightTuning.gravity,
209
+ thrust: flightTuning.thrust,
210
+ alignWithLook: flightTuning.alignWithLook,
211
+ collide: flightTuning.collide,
212
+ bindings: flightTuning.bindings,
213
+ camera: flightTuning.camera,
214
+ };
215
+ if (normalizedFlightTuning.collide === undefined) {
216
+ normalizedFlightTuning.collide = normalizedFlightTuning.mode === "creative" || normalizedFlightTuning.mode === "hover";
217
+ }
218
+ if (normalizedFlightTuning.camera !== undefined) {
219
+ const cam = ctx.camera;
220
+ cam?.setChaseTuning(normalizedFlightTuning.camera ?? null);
221
+ }
222
+ const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning);
223
+ let stepX = step.stepX;
224
+ let stepY = step.stepY;
225
+ let stepZ = step.stepZ;
226
+ if (normalizedFlightTuning.collide !== false && tuning.movement?.collideObjects !== false) {
227
+ const obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ);
228
+ const verticalResolved = resolveFlightStep(player.position, stepX, stepY, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS);
229
+ stepY = verticalResolved.stepY;
230
+ const xzResolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, 0);
231
+ stepX = xzResolved.stepX;
232
+ stepZ = xzResolved.stepZ;
233
+ const ground = tuning.ground.sampleHeight(player.position[0] + stepX, player.position[2] + stepZ);
234
+ const nextY = player.position[1] + stepY;
235
+ if (nextY < ground + 0.2) {
236
+ stepY = ground + 0.2 - player.position[1];
237
+ if (flightState.vy < 0)
238
+ flightState.vy = 0;
239
+ }
240
+ }
241
+ let nextX = player.position[0] + stepX;
242
+ let nextY = player.position[1] + stepY;
243
+ let nextZ = player.position[2] + stepZ;
244
+ if (motionBatch !== null && motionBatch.y !== null) {
245
+ nextY = motionBatch.y;
246
+ }
247
+ if (tuning.movement?.beforeCommit !== undefined) {
248
+ const frame = {
249
+ entityId: playerId,
250
+ current: player.position,
251
+ next: [nextX, nextY, nextZ],
252
+ dt,
253
+ ctx,
254
+ };
255
+ const replacement = tuning.movement.beforeCommit(frame);
256
+ if (replacement !== undefined) {
257
+ nextX = replacement[0];
258
+ nextY = replacement[1];
259
+ nextZ = replacement[2];
260
+ }
261
+ }
262
+ ctx.scene.entity.setPose(playerId, {
263
+ position: [nextX, nextY, nextZ],
264
+ rotationY: resolveBodyFacing(state, flightIntent.moving, flightState.vx, flightState.vz, player.rotationY, tuning.movement?.turnSpeed, dt),
265
+ dt,
266
+ });
267
+ return;
268
+ }
269
+ if (flightTuning === null && state.flight !== null) {
270
+ const cam = ctx.camera;
271
+ cam?.setChaseTuning(null);
272
+ }
273
+ if (tuning.controller !== undefined) {
274
+ let controller = state.controller;
275
+ if (controller === null) {
276
+ controller = createCharacterController(tuning.controller.capsule);
277
+ state.controller = controller;
278
+ controller.restore({
279
+ position: [player.position[0], player.position[1], player.position[2]],
280
+ verticalVelocity: 0,
281
+ grounded: true,
282
+ groundNormal: [0, 1, 0],
283
+ groundBody: null,
284
+ crouching: false,
285
+ });
286
+ }
287
+ const controllerState = controller.state();
288
+ if (Math.hypot(controllerState.position[0] - player.position[0], controllerState.position[1] - player.position[1], controllerState.position[2] - player.position[2]) > 1e-6) {
289
+ controller.restore({
290
+ ...controllerState,
291
+ position: [player.position[0], player.position[1], player.position[2]],
292
+ grounded: false,
293
+ groundBody: null,
294
+ });
295
+ }
296
+ // Keep the existing input feel and horizontal impulse seam; the capsule owns vertical motion and collision.
297
+ let horizontal = state.motion;
298
+ if (horizontal === null) {
299
+ horizontal = createPlayerMotionState();
300
+ state.motion = horizontal;
301
+ }
302
+ const grounded = controller.state().grounded;
303
+ horizontal.grounded = grounded;
304
+ horizontal.verticalVelocity = 0;
305
+ horizontal.jumpOffset = 0;
306
+ [horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ] = applyHorizontalImpulses(horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, motionBatch);
307
+ const jumpPressed = intent.jumping && !state.controllerJumpHeld;
308
+ const horizontalStep = advancePlayerMotion(horizontal, { ...intent, jumping: false }, forwardX, forwardZ, walkSpeed, dt, tuning.physics);
309
+ horizontal.grounded = grounded;
310
+ horizontal.verticalVelocity = 0;
311
+ horizontal.jumpOffset = 0;
312
+ state.controllerJumpHeld = intent.jumping;
313
+ const current = controller.state();
314
+ controller.restore({
315
+ ...current,
316
+ verticalVelocity: applyMotionImpulses(current.verticalVelocity, motionBatch),
317
+ });
318
+ const result = controller.move(tuning.controller.backend, {
319
+ motion: [horizontalStep.stepX, 0, horizontalStep.stepZ],
320
+ dt,
321
+ gravity: tuning.physics?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration,
322
+ ...(jumpPressed && !intent.crouching
323
+ ? { jumpVelocity: tuning.physics?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity }
324
+ : {}),
325
+ crouch: intent.crouching,
326
+ });
327
+ let nextPosition = [
328
+ controller.state().position[0],
329
+ controller.state().position[1],
330
+ controller.state().position[2],
331
+ ];
332
+ if (motionBatch !== null && motionBatch.y !== null) {
333
+ nextPosition[1] = motionBatch.y;
334
+ controller.restore({ ...controller.state(), position: nextPosition });
335
+ }
336
+ if (tuning.movement?.beforeCommit !== undefined) {
337
+ const frame = {
338
+ entityId: playerId,
339
+ current: player.position,
340
+ next: nextPosition,
341
+ dt,
342
+ ctx,
343
+ };
344
+ const replacement = tuning.movement.beforeCommit(frame);
345
+ if (replacement !== undefined) {
346
+ nextPosition = [replacement[0], replacement[1], replacement[2]];
347
+ controller.restore({ ...controller.state(), position: nextPosition });
348
+ }
349
+ }
350
+ horizontal.grounded = result.grounded;
351
+ ctx.scene.entity.setPose(playerId, {
352
+ position: nextPosition,
353
+ rotationY: resolveBodyFacing(state, intent.moving, horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
354
+ dt,
355
+ });
356
+ return;
357
+ }
144
358
  if (tuning.collision?.voxel === true) {
145
359
  let body = state.voxelBody;
146
360
  if (body === null) {
@@ -0,0 +1,5 @@
1
+ import type { GameContext } from "../runtime/gameContext.js";
2
+ import type { InputFrame } from "../runtime/inputSnapshot.js";
3
+ import { type PlayerMovementTuning } from "./playerMovement.js";
4
+ /** Advance one authoritative player's movement using the same controller as the shell. */
5
+ export declare function serverStep(ctx: GameContext, userId: string, frame: InputFrame, dt: number, tuning?: PlayerMovementTuning): void;
@@ -0,0 +1,6 @@
1
+ import { groundFieldFor } from "../world/terrain.js";
2
+ import { stepPlayerMovement } from "./playerMovement.js";
3
+ /** Advance one authoritative player's movement using the same controller as the shell. */
4
+ export function serverStep(ctx, userId, frame, dt, tuning) {
5
+ stepPlayerMovement(ctx, userId, frame, dt, tuning ?? { ground: groundFieldFor(), hasTerrain: false });
6
+ }
@@ -11,8 +11,8 @@ export interface ObstacleReach {
11
11
  }
12
12
  /**
13
13
  * The slice of the scene's object store this query needs. Narrow on purpose: the scene context wires
14
- * its own store here while it is still being constructed, and `ctx.scene.object` satisfies it as-is.
15
- * @internal
14
+ * its own store here while it is still being constructed, `ctx.scene.object` satisfies it as-is, and a
15
+ * pure rules package with no `GameContext` to import can satisfy it from its own object list.
16
16
  */
17
17
  export interface SolidObstacleSource {
18
18
  list(): readonly {
@@ -27,12 +27,12 @@ export interface SolidObstacleSource {
27
27
  }[];
28
28
  collidersOf(instanceId: string): Parameters<typeof resolveColliders>[0] | null;
29
29
  }
30
- /** Mutable reach cache; one per source, invalidated by object count. @internal */
30
+ /** Mutable reach cache; one per source, invalidated by object count. */
31
31
  export interface ObstacleReachCache {
32
32
  count: number;
33
33
  value: ObstacleReach;
34
34
  }
35
- /** A fresh {@link ObstacleReachCache} for a caller that owns its own source. @internal */
35
+ /** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
36
36
  export declare function createObstacleReachCache(): ObstacleReachCache;
37
37
  /**
38
38
  * Largest blocking reach in the scene, split into horizontal vs vertical and hard-capped. Cached per
@@ -41,7 +41,7 @@ export declare function createObstacleReachCache(): ObstacleReachCache;
41
41
  * @internal
42
42
  */
43
43
  export declare function solidObstacleReach(ctx: GameContext): ObstacleReach;
44
- /** {@link solidObstacleReach} against a bare source and caller-owned cache. @internal */
44
+ /** {@link solidObstacleReach} against a bare source and caller-owned cache. */
45
45
  export declare function sourceObstacleReach(source: SolidObstacleSource, cache: ObstacleReachCache): ObstacleReach;
46
46
  /** One blocking collider as a {@link CollisionObstacle}: a compound `boxes` obstacle for a mesh collider
47
47
  * carrying a small sub-box set (openings), else a single half-extents/offset AABB. Yaw is applied by
@@ -61,7 +61,7 @@ export declare function obstacleFromCollider(collider: ResolvedCollider, positio
61
61
  * @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
62
62
  */
63
63
  export declare function solidObstaclesNear(ctx: GameContext, position: EntityPosition, reachX: number, reachZ: number, height?: number): CollisionObstacle[];
64
- /** {@link solidObstaclesNear} against a bare source and an already-resolved reach. @internal */
64
+ /** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
65
65
  export declare function sourceObstaclesNear(source: SolidObstacleSource, reach: ObstacleReach, position: EntityPosition, reachX: number, reachZ: number, height?: number): CollisionObstacle[];
66
66
  /**
67
67
  * Slide a walker's horizontal step against the same solid geometry that stops the player, returning the
@@ -71,6 +71,11 @@ export declare function sourceObstaclesNear(source: SolidObstacleSource, reach:
71
71
  * Bounded by the same broadphase as the player's gather: one `inBox` query sized to this step, never a
72
72
  * full-world scan, so it stays affordable per NPC per tick.
73
73
  *
74
+ * The sliding is already axis-separated: X and Z are resolved independently inside, so a caller that
75
+ * also splits its own movement by axis must hand this exactly one axis per call (`stepZ: 0`, then
76
+ * `stepX: 0`). Running an axis-split pass over a step this function already resolved double-cuts it,
77
+ * and the composition is wrong in either order.
78
+ *
74
79
  * @capability walker-solid-step slide an NPC's step against the geometry that blocks the player
75
80
  */
76
81
  export declare function resolveWalkerStep(ctx: GameContext, position: EntityPosition, stepX: number, stepZ: number, options?: {
@@ -80,7 +85,25 @@ export declare function resolveWalkerStep(ctx: GameContext, position: EntityPosi
80
85
  stepX: number;
81
86
  stepZ: number;
82
87
  };
83
- /** {@link resolveWalkerStep} against an already-gathered obstacle set. @internal */
88
+ /**
89
+ * {@link resolveWalkerStep} with no `GameContext`: gather plus slide against a bare
90
+ * {@link SolidObstacleSource} and a caller-owned {@link ObstacleReachCache}.
91
+ *
92
+ * This is the entry point for rules that live in a pure package — no React, no three.js, no engine
93
+ * context to import. Hold one cache per source for the life of that source (`createObstacleReachCache()`)
94
+ * rather than making a fresh one per step; it is what keeps the reach scan off the per-step path.
95
+ * The same axis-separation note as {@link resolveWalkerStep} applies.
96
+ *
97
+ * @capability walker-solid-step-source slide a step against solid geometry from a pure rules package
98
+ */
99
+ export declare function resolveSourceWalkerStep(source: SolidObstacleSource, cache: ObstacleReachCache, position: EntityPosition, stepX: number, stepZ: number, options?: {
100
+ radius?: number;
101
+ stepUpHeight?: number;
102
+ }): {
103
+ stepX: number;
104
+ stepZ: number;
105
+ };
106
+ /** {@link resolveWalkerStep} against an already-gathered obstacle set. */
84
107
  export declare function slideStep(position: EntityPosition, stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius?: number, stepUpHeight?: number): {
85
108
  stepX: number;
86
109
  stepZ: number;
@@ -28,7 +28,7 @@ const OBSTACLE_VERTICAL_CAP = 8;
28
28
  const MOVEMENT_MESH_BOX_BUDGET = 12;
29
29
  /** Feet-to-head span a walker is tested over — matches the player's, so both agree on what a wall is. */
30
30
  const WALKER_HEIGHT = 1.8;
31
- /** A fresh {@link ObstacleReachCache} for a caller that owns its own source. @internal */
31
+ /** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
32
32
  export function createObstacleReachCache() {
33
33
  return { count: -1, value: { horizontal: OBSTACLE_MAX_HALF_EXTENT, vertical: OBSTACLE_VERTICAL_FLOOR } };
34
34
  }
@@ -42,7 +42,7 @@ const reachCache = perContext(() => createObstacleReachCache());
42
42
  export function solidObstacleReach(ctx) {
43
43
  return sourceObstacleReach(ctx.scene.object, reachCache(ctx));
44
44
  }
45
- /** {@link solidObstacleReach} against a bare source and caller-owned cache. @internal */
45
+ /** {@link solidObstacleReach} against a bare source and caller-owned cache. */
46
46
  export function sourceObstacleReach(source, cache) {
47
47
  const objects = source.list();
48
48
  if (cache.count === objects.length)
@@ -155,7 +155,7 @@ export function obstacleFromCollider(collider, position, rotationY) {
155
155
  export function solidObstaclesNear(ctx, position, reachX, reachZ, height = WALKER_HEIGHT) {
156
156
  return sourceObstaclesNear(ctx.scene.object, solidObstacleReach(ctx), position, reachX, reachZ, height);
157
157
  }
158
- /** {@link solidObstaclesNear} against a bare source and an already-resolved reach. @internal */
158
+ /** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
159
159
  export function sourceObstaclesNear(source, reach, position, reachX, reachZ, height = WALKER_HEIGHT) {
160
160
  const min = [
161
161
  position[0] - reachX - reach.horizontal,
@@ -191,6 +191,11 @@ export function sourceObstaclesNear(source, reach, position, reachX, reachZ, hei
191
191
  * Bounded by the same broadphase as the player's gather: one `inBox` query sized to this step, never a
192
192
  * full-world scan, so it stays affordable per NPC per tick.
193
193
  *
194
+ * The sliding is already axis-separated: X and Z are resolved independently inside, so a caller that
195
+ * also splits its own movement by axis must hand this exactly one axis per call (`stepZ: 0`, then
196
+ * `stepX: 0`). Running an axis-split pass over a step this function already resolved double-cuts it,
197
+ * and the composition is wrong in either order.
198
+ *
194
199
  * @capability walker-solid-step slide an NPC's step against the geometry that blocks the player
195
200
  */
196
201
  export function resolveWalkerStep(ctx, position, stepX, stepZ, options = {}) {
@@ -200,7 +205,25 @@ export function resolveWalkerStep(ctx, position, stepX, stepZ, options = {}) {
200
205
  const obstacles = solidObstaclesNear(ctx, position, Math.abs(stepX) + radius, Math.abs(stepZ) + radius);
201
206
  return slideStep(position, stepX, stepZ, obstacles, radius, options.stepUpHeight ?? 0);
202
207
  }
203
- /** {@link resolveWalkerStep} against an already-gathered obstacle set. @internal */
208
+ /**
209
+ * {@link resolveWalkerStep} with no `GameContext`: gather plus slide against a bare
210
+ * {@link SolidObstacleSource} and a caller-owned {@link ObstacleReachCache}.
211
+ *
212
+ * This is the entry point for rules that live in a pure package — no React, no three.js, no engine
213
+ * context to import. Hold one cache per source for the life of that source (`createObstacleReachCache()`)
214
+ * rather than making a fresh one per step; it is what keeps the reach scan off the per-step path.
215
+ * The same axis-separation note as {@link resolveWalkerStep} applies.
216
+ *
217
+ * @capability walker-solid-step-source slide a step against solid geometry from a pure rules package
218
+ */
219
+ export function resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options = {}) {
220
+ if (stepX === 0 && stepZ === 0)
221
+ return { stepX, stepZ };
222
+ const radius = options.radius ?? DEFAULT_OBSTACLE_PLAYER_RADIUS;
223
+ const obstacles = sourceObstaclesNear(source, sourceObstacleReach(source, cache), position, Math.abs(stepX) + radius, Math.abs(stepZ) + radius);
224
+ return slideStep(position, stepX, stepZ, obstacles, radius, options.stepUpHeight ?? 0);
225
+ }
226
+ /** {@link resolveWalkerStep} against an already-gathered obstacle set. */
204
227
  export function slideStep(position, stepX, stepZ, obstacles, radius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0) {
205
228
  if (obstacles.length === 0)
206
229
  return { stepX, stepZ };
@@ -16,4 +16,6 @@ export interface PoseSyncTuning {
16
16
  export interface PoseSyncGate {
17
17
  evaluate(pose: PlayerPose, nowMs: number): boolean;
18
18
  }
19
- export declare function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate;
19
+ /** Default ten-hertz client pose gate with a five-second heartbeat. */
20
+ export declare const DEFAULT_POSE_SYNC_TUNING: PoseSyncTuning;
21
+ export declare function createPoseSyncGate(tuning?: PoseSyncTuning): PoseSyncGate;
@@ -17,7 +17,15 @@ function poseChanged(a, b, tuning) {
17
17
  || Math.abs(a.rotationPitch - b.rotationPitch) > tuning.rotationEpsilon
18
18
  || appearanceChanged(a.appearance, b.appearance));
19
19
  }
20
- export function createPoseSyncGate(tuning) {
20
+ /** Default ten-hertz client pose gate with a five-second heartbeat. */
21
+ export const DEFAULT_POSE_SYNC_TUNING = {
22
+ minIntervalMs: 100,
23
+ heartbeatMs: 5_000,
24
+ positionEpsilon: 0.01,
25
+ verticalEpsilon: 0.01,
26
+ rotationEpsilon: 0.01,
27
+ };
28
+ export function createPoseSyncGate(tuning = DEFAULT_POSE_SYNC_TUNING) {
21
29
  let lastSentPose = null;
22
30
  let lastSentAt = 0;
23
31
  return {
@@ -16,6 +16,12 @@ export interface PresencePose {
16
16
  export interface PresenceSession<TGameId extends string = string> {
17
17
  homeGameId: TGameId;
18
18
  externalId: string;
19
+ viewerChunkKey?: string;
20
+ }
21
+ /** Resident identity used to suppress offline actors while their owner is online. */
22
+ export interface PresenceResidentRow {
23
+ actorExternalId: string;
24
+ ownerActorId?: string;
19
25
  }
20
26
  export interface EnsurePresenceArgs<TGameId extends string = string> {
21
27
  gameId: TGameId;
@@ -22,6 +22,8 @@ export interface IncomingPose {
22
22
  export interface PoseSyncRules {
23
23
  /** Speed cap (units/sec) for client-authoritative movement. */
24
24
  maxSpeed: number;
25
+ /** Minimum time between accepted pose writes. Defaults to 100 ms. */
26
+ minIntervalMs?: number;
25
27
  /** Vertical offset clamp above floorY (e.g. peak jump height). */
26
28
  maxVerticalOffset: number;
27
29
  /** World-floor Y used as the base of the jump band. Defaults to 0. */
@@ -1,6 +1,7 @@
1
1
  /** Canonical client-authoritative pose-sync tuning shared by every host transport (WS, Convex). */
2
2
  export const DEFAULT_POSE_SYNC_RULES = {
3
3
  maxSpeed: 12,
4
+ minIntervalMs: 100,
4
5
  maxVerticalOffset: 3,
5
6
  minElapsedSec: 0.05,
6
7
  maxElapsedSec: 0.5,
@@ -29,6 +30,16 @@ export function spawnPresenceState(origin = DEFAULT_SPAWN_ORIGIN, nowMs, rules)
29
30
  }
30
31
  /** @internal */
31
32
  export function decidePoseSync(current, incoming, rules, nowMs, floorY) {
33
+ if (current.lastSeenAtMs !== undefined && nowMs - current.lastSeenAtMs < (rules.minIntervalMs ?? 100)) {
34
+ return {
35
+ position: { ...current.position },
36
+ rotationY: current.rotationY,
37
+ rotationPitch: current.rotationPitch ?? 0,
38
+ appearance: current.appearance,
39
+ changed: false,
40
+ refreshKeepAlive: false,
41
+ };
42
+ }
32
43
  const rawElapsedSec = Math.max(0, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000);
33
44
  const elapsedSec = Math.min(rules.maxElapsedSec, rawElapsedSec);
34
45
  const maxDist = rules.maxSpeed * elapsedSec;
@@ -0,0 +1,33 @@
1
+ import type { Vec3 } from "../world/geometry.js";
2
+ /** Explicit traversable connection between two navigation polygons. */
3
+ export interface NavMeshLink {
4
+ from: number;
5
+ to: number;
6
+ cost?: number;
7
+ }
8
+ /** Serializable polygon navigation mesh data. */
9
+ export interface NavMeshData {
10
+ /** Flat xyz vertex coordinates. Polygon indices refer to triples in this array. */
11
+ verts: number[];
12
+ polys: number[][];
13
+ links: NavMeshLink[];
14
+ areas?: number[];
15
+ }
16
+ /** Neighbor relationship for one navigation polygon. */
17
+ export interface NavMeshAdjacency {
18
+ neighbors: number[];
19
+ cost: number;
20
+ }
21
+ /** Route points and polygons selected through a navigation mesh. */
22
+ export interface NavMeshPath {
23
+ points: Vec3[];
24
+ polys: number[];
25
+ }
26
+ /** Build polygon adjacency from shared edges and explicit off-mesh links. */
27
+ export declare function buildNavAdjacency(mesh: NavMeshData): NavMeshAdjacency[];
28
+ /** Return the closest point on the mesh surface, or null for an empty mesh. */
29
+ export declare function closestPoint(mesh: NavMeshData, point: Vec3): Vec3 | null;
30
+ /** A* over polygon centers, followed by deterministic visibility string-pulling. */
31
+ export declare function findPath(mesh: NavMeshData, from: Vec3, to: Vec3): NavMeshPath | null;
32
+ /** True when the segment remains over walkable polygons. */
33
+ export declare function raycastNav(mesh: NavMeshData, from: Vec3, to: Vec3): boolean;