@jgengine/core 0.18.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 +114 -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 +85 -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 +12 -7
  90. package/dist/runtime/commandRunner.js +7 -5
  91. package/dist/runtime/gameContext.js +11 -1
  92. package/dist/runtime/gameContextTypes.d.ts +14 -4
  93. package/dist/runtime/gameRuntime.d.ts +20 -4
  94. package/dist/runtime/gameRuntime.js +17 -4
  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 +32 -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 +94 -19
  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
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Keeps local input predictions ahead of the last server confirmation. Reconciliation replaces the
3
+ * confirmed state and deterministically replays unconfirmed inputs through the supplied step function.
4
+ * `maxTicks` bounds retained work and memory; dropped inputs are folded into the confirmed baseline.
5
+ * @capability client-side input prediction with replay reconciliation
6
+ */
7
+ export function createPredictionBuffer(config) {
8
+ const maxTicks = Math.max(1, Math.floor(config.maxTicks));
9
+ const step = config.step;
10
+ let confirmedTick = config.startTick ?? 0;
11
+ let confirmed = config.initial;
12
+ let predicted = config.initial;
13
+ let records = [];
14
+ const defaultDt = config.dt ?? 1 / 60;
15
+ const snapThreshold = Math.max(0, config.snapThreshold ?? 0.1);
16
+ const replay = (base, pending) => {
17
+ let next = base;
18
+ for (const record of pending)
19
+ next = step(next, record.input, record.dt);
20
+ return next;
21
+ };
22
+ return {
23
+ record(input, dt = defaultDt, observedState) {
24
+ const tick = confirmedTick + records.length + 1;
25
+ const state = observedState === undefined ? step(predicted, input, dt) : observedState;
26
+ records.push({ tick, input, state, dt });
27
+ predicted = state;
28
+ if (records.length > maxTicks) {
29
+ const oldest = records.shift();
30
+ confirmedTick = oldest.tick;
31
+ confirmed = oldest.state;
32
+ }
33
+ return predicted;
34
+ },
35
+ reconcile(tick, authoritative) {
36
+ const predictedAtTick = records.find((record) => record.tick === tick)?.state ?? confirmed;
37
+ const distance = (a, b) => {
38
+ if (typeof a === "number" && typeof b === "number")
39
+ return Math.abs(a - b);
40
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v) => typeof v === "number") && b.every((v) => typeof v === "number")) {
41
+ return Math.sqrt(a.reduce((sum, value, index) => sum + (Number(value) - Number(b[index])) ** 2, 0));
42
+ }
43
+ return JSON.stringify(a) === JSON.stringify(b) ? 0 : 1;
44
+ };
45
+ const dx = distance(predictedAtTick, authoritative);
46
+ const pending = records.filter((record) => record.tick > tick);
47
+ confirmedTick = tick;
48
+ confirmed = authoritative;
49
+ records = pending;
50
+ predicted = replay(confirmed, records);
51
+ return { error: dx, replayed: records.length, snapped: dx > snapThreshold, state: predicted };
52
+ },
53
+ state: () => predicted,
54
+ snapshot: () => ({ confirmedTick, confirmed, predicted, records: records.map((record) => ({ ...record })) }),
55
+ restore(next) {
56
+ confirmedTick = next.confirmedTick;
57
+ confirmed = next.confirmed;
58
+ predicted = next.predicted;
59
+ records = next.records.map((record) => ({ ...record }));
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,45 @@
1
+ import type { EntityStore } from "../scene/entityStore.js";
2
+ import { type PoseHistory, type RenderPose } from "./poseInterpolation.js";
3
+ import { type SimAdvanceResult, type SimLoop, type SimulationConfig } from "./simLoop.js";
4
+ /** Where a registered stage runs inside one simulation step. */
5
+ export type SimStagePhase = "beforeMovement" | "afterMovement" | "afterTick";
6
+ /** Extra work a game or engine seam (a physics backend, a netcode buffer) runs inside every simulation step. */
7
+ export interface SimStage {
8
+ id: string;
9
+ phase: SimStagePhase;
10
+ run(dt: number, tick: number): void;
11
+ }
12
+ /** Serializable `ctx.sim` state: loop position plus the interpolation pose history. */
13
+ export interface SimContextState {
14
+ loop: ReturnType<SimLoop["snapshot"]>;
15
+ poses: ReturnType<PoseHistory["snapshot"]>;
16
+ }
17
+ /**
18
+ * `ctx.sim`: the simulation stepper shared by every driver plus the render-side interpolation it enables.
19
+ * Drivers call {@link advance} with real elapsed seconds and do their per-step work inside the callback;
20
+ * renderers call {@link renderPose} each frame.
21
+ */
22
+ export interface SimContext {
23
+ readonly loop: SimLoop;
24
+ /** Advance the loop; `body` runs once per simulation step with that step's dt. Pose history is captured before each step. */
25
+ advance(realDt: number, body: (dt: number, tick: number) => void): SimAdvanceResult;
26
+ tick(): number;
27
+ /** Interpolation fraction for the current frame, 1 on a variable loop. */
28
+ alpha(): number;
29
+ /** Interpolated pose for rendering; live pose on a variable loop. `false` when the entity does not exist. */
30
+ renderPose(id: string, out: RenderPose): boolean;
31
+ /** Forget an entity's interpolation history so it renders at its live pose next frame. */
32
+ snapPose(id: string): void;
33
+ /** Register a stage; returns an unregister handle. Stages run in registration order within a phase. */
34
+ addStage(stage: SimStage): () => void;
35
+ /** Run every stage registered for `phase`; drivers call this from inside {@link advance}'s body. */
36
+ runStages(phase: SimStagePhase, dt: number): void;
37
+ retune(config: SimulationConfig): void;
38
+ snapshot(): SimContextState;
39
+ restore(state: SimContextState): void;
40
+ }
41
+ /** Build `ctx.sim` for a context; `createGameContext` calls this from `definition.simulation`. */
42
+ export declare function createSimContext(options: {
43
+ config?: SimulationConfig;
44
+ entities: EntityStore;
45
+ }): SimContext;
@@ -0,0 +1,65 @@
1
+ import { createPoseHistory } from "./poseInterpolation.js";
2
+ import { createSimLoop } from "./simLoop.js";
3
+ /** Build `ctx.sim` for a context; `createGameContext` calls this from `definition.simulation`. */
4
+ export function createSimContext(options) {
5
+ const { entities } = options;
6
+ const loop = createSimLoop(options.config);
7
+ const poses = createPoseHistory({ snapDistance: loop.config().snapDistance });
8
+ const stages = new Map();
9
+ let advancing = false;
10
+ return {
11
+ loop,
12
+ advance(realDt, body) {
13
+ if (advancing)
14
+ throw new Error("ctx.sim.advance is not reentrant");
15
+ advancing = true;
16
+ try {
17
+ return loop.advance(realDt, (dt, tick) => {
18
+ if (loop.isFixed())
19
+ poses.beginStep(entities);
20
+ body(dt, tick);
21
+ });
22
+ }
23
+ finally {
24
+ advancing = false;
25
+ }
26
+ },
27
+ tick: () => loop.tick(),
28
+ alpha: () => loop.alpha(),
29
+ renderPose(id, out) {
30
+ return poses.sample(entities, id, loop.isFixed() ? loop.alpha() : 1, out);
31
+ },
32
+ snapPose: (id) => poses.snap(id),
33
+ addStage(stage) {
34
+ const list = stages.get(stage.phase) ?? [];
35
+ list.push(stage);
36
+ stages.set(stage.phase, list);
37
+ return () => {
38
+ const current = stages.get(stage.phase);
39
+ if (current === undefined)
40
+ return;
41
+ const index = current.indexOf(stage);
42
+ if (index >= 0)
43
+ current.splice(index, 1);
44
+ };
45
+ },
46
+ runStages(phase, dt) {
47
+ const list = stages.get(phase);
48
+ if (list === undefined)
49
+ return;
50
+ const tick = loop.tick();
51
+ for (const stage of list)
52
+ stage.run(dt, tick);
53
+ },
54
+ retune(config) {
55
+ loop.retune(config);
56
+ if (config.snapDistance !== undefined)
57
+ poses.retune({ snapDistance: config.snapDistance });
58
+ },
59
+ snapshot: () => ({ loop: loop.snapshot(), poses: poses.snapshot() }),
60
+ restore(state) {
61
+ loop.restore(state.loop);
62
+ poses.restore(state.poses);
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,52 @@
1
+ /** How the simulation is stepped: a fixed rate with a catch-up cap, or once per frame with the frame's dt. */
2
+ export interface SimulationConfig {
3
+ /**
4
+ * Fixed simulation rate in steps per second, or `"variable"` to run one step per frame with the frame's
5
+ * clamped dt. Default `"variable"` (the historical behavior). A fixed rate makes every step's dt identical,
6
+ * which is what determinism, replay, rollback, and a stable physics solver need; renderers read
7
+ * `ctx.sim.renderPose` to interpolate between steps.
8
+ */
9
+ hz?: number | "variable";
10
+ /** Fixed steps run per advance before the loop drops the remaining time instead of spiralling. Default 5. */
11
+ maxCatchUpSteps?: number;
12
+ /** Entities that move farther than this in one step snap instead of interpolating (teleports, respawns). Default 8 world units. */
13
+ snapDistance?: number;
14
+ }
15
+ /** Serializable loop position: the tick count and the real seconds accumulated toward the next fixed step. */
16
+ export interface SimLoopState {
17
+ tick: number;
18
+ accumulator: number;
19
+ }
20
+ /** What one {@link SimLoop.advance} did. */
21
+ export interface SimAdvanceResult {
22
+ /** Steps run this advance. */
23
+ steps: number;
24
+ /** Fraction of a step accumulated but not yet simulated, 0..1; always 1 on a variable loop. */
25
+ alpha: number;
26
+ /** Tick count after this advance. */
27
+ tick: number;
28
+ /** Steps discarded because the catch-up cap was hit. */
29
+ dropped: number;
30
+ }
31
+ /** The stepper handle: advance with real time, read the tick and interpolation alpha, retune the rate, snapshot and restore. */
32
+ export interface SimLoop {
33
+ /** Feed real elapsed seconds; runs `step(dt, tick)` zero or more times and returns what happened. */
34
+ advance(realDt: number, step: (dt: number, tick: number) => void): SimAdvanceResult;
35
+ tick(): number;
36
+ alpha(): number;
37
+ /** Seconds per fixed step, `null` on a variable loop. */
38
+ stepSeconds(): number | null;
39
+ isFixed(): boolean;
40
+ retune(config: SimulationConfig): void;
41
+ config(): Readonly<Required<SimulationConfig>>;
42
+ snapshot(): SimLoopState;
43
+ restore(state: SimLoopState): void;
44
+ }
45
+ /**
46
+ * The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so
47
+ * the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in
48
+ * equal slices with a catch-up cap; variable mode steps once per advance.
49
+ *
50
+ * @capability sim-loop fixed-step or variable simulation stepping with tick count, catch-up cap, and interpolation alpha
51
+ */
52
+ export declare function createSimLoop(initial?: SimulationConfig): SimLoop;
@@ -0,0 +1,72 @@
1
+ const DEFAULT_MAX_CATCH_UP = 5;
2
+ const DEFAULT_SNAP_DISTANCE = 8;
3
+ function resolveConfig(config) {
4
+ const hz = config?.hz;
5
+ return {
6
+ hz: typeof hz === "number" && Number.isFinite(hz) && hz > 0 ? hz : "variable",
7
+ maxCatchUpSteps: Math.max(1, Math.floor(config?.maxCatchUpSteps ?? DEFAULT_MAX_CATCH_UP)),
8
+ snapDistance: config?.snapDistance ?? DEFAULT_SNAP_DISTANCE,
9
+ };
10
+ }
11
+ /**
12
+ * The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so
13
+ * the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in
14
+ * equal slices with a catch-up cap; variable mode steps once per advance.
15
+ *
16
+ * @capability sim-loop fixed-step or variable simulation stepping with tick count, catch-up cap, and interpolation alpha
17
+ */
18
+ export function createSimLoop(initial) {
19
+ let config = resolveConfig(initial);
20
+ let tick = 0;
21
+ let accumulator = 0;
22
+ let alpha = 1;
23
+ function stepSeconds() {
24
+ return config.hz === "variable" ? null : 1 / config.hz;
25
+ }
26
+ return {
27
+ advance(realDt, step) {
28
+ if (!(realDt > 0))
29
+ return { steps: 0, alpha, tick, dropped: 0 };
30
+ const stepDt = stepSeconds();
31
+ if (stepDt === null) {
32
+ tick += 1;
33
+ alpha = 1;
34
+ step(realDt, tick);
35
+ return { steps: 1, alpha, tick, dropped: 0 };
36
+ }
37
+ accumulator += realDt;
38
+ let steps = 0;
39
+ while (accumulator >= stepDt && steps < config.maxCatchUpSteps) {
40
+ tick += 1;
41
+ step(stepDt, tick);
42
+ accumulator -= stepDt;
43
+ steps += 1;
44
+ }
45
+ let dropped = 0;
46
+ if (accumulator >= stepDt) {
47
+ dropped = Math.floor(accumulator / stepDt);
48
+ accumulator -= dropped * stepDt;
49
+ }
50
+ alpha = accumulator / stepDt;
51
+ return { steps, alpha, tick, dropped };
52
+ },
53
+ tick: () => tick,
54
+ alpha: () => alpha,
55
+ stepSeconds,
56
+ isFixed: () => config.hz !== "variable",
57
+ retune(next) {
58
+ config = resolveConfig({ ...config, ...next });
59
+ if (config.hz === "variable") {
60
+ accumulator = 0;
61
+ alpha = 1;
62
+ }
63
+ },
64
+ config: () => config,
65
+ snapshot: () => ({ tick, accumulator }),
66
+ restore(state) {
67
+ tick = state.tick;
68
+ accumulator = state.accumulator;
69
+ alpha = config.hz === "variable" ? 1 : accumulator * config.hz;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,32 @@
1
+ import type { GameContext } from "./gameContext.js";
2
+ /** One piece of simulation state that can be captured and restored by id. */
3
+ export interface SimSnapshotContributor<T = unknown> {
4
+ id: string;
5
+ capture(): T;
6
+ restore(data: T): void;
7
+ }
8
+ /** Captured contributor data keyed by contributor id. */
9
+ export type SimSnapshot = Record<string, unknown>;
10
+ /** Registry of {@link SimSnapshotContributor}s that captures and restores them together. */
11
+ export interface SimSnapshotRegistry {
12
+ register<T>(contributor: SimSnapshotContributor<T>): () => void;
13
+ capture(): SimSnapshot;
14
+ /** Alias of {@link capture}, matching the engine's snapshot/restore naming. */
15
+ snapshot(): SimSnapshot;
16
+ /** Restore every contributor present in `snapshot`; contributors missing from it are left untouched. */
17
+ restore(snapshot: SimSnapshot): void;
18
+ ids(): string[];
19
+ }
20
+ /**
21
+ * A registry of everything that must round-trip for a simulation to resume bit-for-bit: world state, the
22
+ * clock, the rng cursor, the loop accumulator, and whatever a physics backend or game adds. Pair with
23
+ * {@link createInputRecorder} for replay.
24
+ *
25
+ * @capability sim-snapshot capture and restore whole-simulation state for replay, rollback, and regression tests
26
+ */
27
+ export declare function createSimSnapshotRegistry(): SimSnapshotRegistry;
28
+ /**
29
+ * The engine's own contributors for a context: replicated world state, the sim clock, the loop and pose
30
+ * history, and the rng cursor when `ctx.rng` is a seeded stream. Games register theirs on the same registry.
31
+ */
32
+ export declare function createContextSimSnapshot(ctx: GameContext): SimSnapshotRegistry;
@@ -0,0 +1,60 @@
1
+ import { rngStateOf, restoreRng } from "../random/rng.js";
2
+ /**
3
+ * A registry of everything that must round-trip for a simulation to resume bit-for-bit: world state, the
4
+ * clock, the rng cursor, the loop accumulator, and whatever a physics backend or game adds. Pair with
5
+ * {@link createInputRecorder} for replay.
6
+ *
7
+ * @capability sim-snapshot capture and restore whole-simulation state for replay, rollback, and regression tests
8
+ */
9
+ export function createSimSnapshotRegistry() {
10
+ const contributors = new Map();
11
+ function capture() {
12
+ const out = {};
13
+ for (const [id, contributor] of contributors)
14
+ out[id] = contributor.capture();
15
+ return out;
16
+ }
17
+ return {
18
+ register(contributor) {
19
+ if (contributors.has(contributor.id)) {
20
+ throw new Error(`sim snapshot contributor "${contributor.id}" is already registered`);
21
+ }
22
+ contributors.set(contributor.id, contributor);
23
+ return () => {
24
+ contributors.delete(contributor.id);
25
+ };
26
+ },
27
+ capture,
28
+ snapshot: capture,
29
+ restore(snapshot) {
30
+ for (const [id, contributor] of contributors) {
31
+ if (id in snapshot)
32
+ contributor.restore(snapshot[id]);
33
+ }
34
+ },
35
+ ids: () => Array.from(contributors.keys()),
36
+ };
37
+ }
38
+ /**
39
+ * The engine's own contributors for a context: replicated world state, the sim clock, the loop and pose
40
+ * history, and the rng cursor when `ctx.rng` is a seeded stream. Games register theirs on the same registry.
41
+ */
42
+ export function createContextSimSnapshot(ctx) {
43
+ const registry = createSimSnapshotRegistry();
44
+ // ctx.snapshot() hands back live rows; clone both ways so a captured snapshot stays frozen while the sim keeps running.
45
+ registry.register({
46
+ id: "world",
47
+ capture: () => structuredClone(ctx.snapshot()),
48
+ restore: (data) => ctx.hydrate(structuredClone(data)),
49
+ });
50
+ registry.register({ id: "clock", capture: () => ctx.time.snapshot(), restore: (data) => ctx.time.hydrate(data) });
51
+ registry.register({ id: "sim", capture: () => ctx.sim.snapshot(), restore: (data) => ctx.sim.restore(data) });
52
+ if (rngStateOf(ctx.rng) !== null) {
53
+ registry.register({
54
+ id: "rng",
55
+ capture: () => rngStateOf(ctx.rng) ?? 0,
56
+ restore: (cursor) => restoreRng(ctx.rng, cursor),
57
+ });
58
+ }
59
+ return registry;
60
+ }
@@ -36,6 +36,8 @@ export type RuntimeInventorySlot = {
36
36
  slot?: number;
37
37
  };
38
38
  export type RuntimePlayerRow = {
39
+ territoryOwnedCount?: number;
40
+ ownedTerritoryChunkKeys?: string[];
39
41
  userId: string;
40
42
  inventories: Record<string, RuntimeInventorySlot[]>;
41
43
  economy: Record<string, number>;
@@ -46,6 +48,12 @@ export type RuntimePlayerRow = {
46
48
  session?: Record<string, unknown>;
47
49
  };
48
50
  export type RuntimeChunkRow = {
51
+ /** Cell key to owner id, persisted with its spatial chunk. */
52
+ territory?: Record<string, string>;
53
+ territoryReceipts?: Record<string, {
54
+ claimedAt: number;
55
+ costPaid: number;
56
+ }>;
49
57
  chunkKey: string;
50
58
  objects: RuntimeObjectRow[];
51
59
  entities: RuntimeEntityRow[];
@@ -0,0 +1,33 @@
1
+ /** A sampled pair of snapshots and interpolation factor. */
2
+ export type SnapshotBufferSample<T> = {
3
+ before: T;
4
+ after: T;
5
+ alpha: number;
6
+ };
7
+ /** Serializable snapshot-buffer state. */
8
+ export type SnapshotBufferState<T> = {
9
+ delayMs: number;
10
+ capacity: number;
11
+ entries: Array<{
12
+ atMs: number;
13
+ value: T;
14
+ }>;
15
+ };
16
+ /** Handle for a timestamped delayed snapshot buffer. */
17
+ export type SnapshotBuffer<T> = {
18
+ push(atMs: number, value: T): void;
19
+ sampleAt(nowMs: number): SnapshotBufferSample<T> | null;
20
+ snapshot(): SnapshotBufferState<T>;
21
+ restore(next: SnapshotBufferState<T>): void;
22
+ retune(config: {
23
+ delayMs?: number;
24
+ capacity?: number;
25
+ }): void;
26
+ };
27
+ /** Stores timestamped snapshots and samples a delayed, interpolatable pair.
28
+ * @capability runtime snapshot interpolation
29
+ */
30
+ export declare function createSnapshotBuffer<T>(config: {
31
+ delayMs: number;
32
+ capacity: number;
33
+ }): SnapshotBuffer<T>;
@@ -0,0 +1,58 @@
1
+ /** Stores timestamped snapshots and samples a delayed, interpolatable pair.
2
+ * @capability runtime snapshot interpolation
3
+ */
4
+ export function createSnapshotBuffer(config) {
5
+ let delayMs = Math.max(0, config.delayMs);
6
+ let capacity = Math.max(1, Math.floor(config.capacity));
7
+ let entries = [];
8
+ const trim = () => {
9
+ if (entries.length > capacity)
10
+ entries = entries.slice(-capacity);
11
+ };
12
+ return {
13
+ push(atMs, value) {
14
+ const entry = { atMs, value };
15
+ const index = entries.findIndex((item) => item.atMs > atMs);
16
+ if (index < 0)
17
+ entries.push(entry);
18
+ else
19
+ entries.splice(index, 0, entry);
20
+ trim();
21
+ },
22
+ sampleAt(nowMs) {
23
+ if (entries.length === 0)
24
+ return null;
25
+ const target = nowMs - delayMs;
26
+ if (target <= entries[0].atMs)
27
+ return { before: entries[0].value, after: entries[0].value, alpha: 0 };
28
+ const last = entries[entries.length - 1];
29
+ if (target >= last.atMs)
30
+ return { before: last.value, after: last.value, alpha: 1 };
31
+ for (let i = 1; i < entries.length; i += 1) {
32
+ const after = entries[i];
33
+ if (target <= after.atMs) {
34
+ const before = entries[i - 1];
35
+ const span = after.atMs - before.atMs;
36
+ return { before: before.value, after: after.value, alpha: span === 0 ? 1 : (target - before.atMs) / span };
37
+ }
38
+ }
39
+ return null;
40
+ },
41
+ snapshot() {
42
+ return { delayMs, capacity, entries: entries.map((entry) => ({ ...entry })) };
43
+ },
44
+ restore(next) {
45
+ delayMs = Math.max(0, next.delayMs);
46
+ capacity = Math.max(1, Math.floor(next.capacity));
47
+ entries = next.entries.map((entry) => ({ ...entry })).sort((a, b) => a.atMs - b.atMs);
48
+ trim();
49
+ },
50
+ retune(config) {
51
+ if (config.delayMs !== undefined)
52
+ delayMs = Math.max(0, config.delayMs);
53
+ if (config.capacity !== undefined)
54
+ capacity = Math.max(1, Math.floor(config.capacity));
55
+ trim();
56
+ },
57
+ };
58
+ }
@@ -1,10 +1,38 @@
1
1
  import type { ChatSync } from "../multiplayer/chatContract.js";
2
2
  import type { PlayerPose } from "../multiplayer/poseSyncGate.js";
3
3
  import type { PresenceTransport } from "../multiplayer/presenceContract.js";
4
+ import type { WorldDiff } from "./worldReplication.js";
5
+ import type { WorldSnapshot } from "./worldSnapshot.js";
6
+ /** A revision-stamped world baseline or compact diff sent to a server-authoritative client. */
7
+ export type WorldSyncFrame = {
8
+ kind: "baseline";
9
+ revision: number;
10
+ snapshot: WorldSnapshot;
11
+ } | {
12
+ kind: "diff";
13
+ revision: number;
14
+ diff: WorldDiff;
15
+ };
16
+ /** A short-lived credential allowing a disconnected client to reclaim one server membership. */
17
+ export type ResumeTicket = {
18
+ userId: string;
19
+ serverId: string;
20
+ token: string;
21
+ };
4
22
  export type JoinServerResult = {
5
23
  serverId: string;
6
24
  isNew: boolean;
25
+ resumeTicket?: ResumeTicket;
26
+ };
27
+ /** Join failures are returned to the caller so the triggering surface can offer retry. */
28
+ export type JoinServerOutcome = (JoinServerResult & {
29
+ ok: true;
30
+ }) | {
31
+ ok: false;
32
+ reason: "full" | "closed" | "unauthorized";
7
33
  };
34
+ /** Connection role used for authoritative world access; spectators are read-only. */
35
+ export type MultiplayerRole = "player" | "spectator";
8
36
  export type RunCommandArgs = {
9
37
  serverId: string;
10
38
  command: string;
@@ -21,7 +49,7 @@ export type GameRuntimeServerView = {
21
49
  gameId: string;
22
50
  revision: number;
23
51
  memberUserIds: string[];
24
- serverState: unknown;
52
+ serverState: unknown | WorldSyncFrame;
25
53
  updatedAt: number;
26
54
  };
27
55
  export type GameRuntimePlayerView = {
@@ -38,7 +66,8 @@ export type GameRuntimeTransport = {
38
66
  joinServer: (args: {
39
67
  gameId: string;
40
68
  serverId?: string;
41
- }) => Promise<JoinServerResult>;
69
+ role?: MultiplayerRole;
70
+ }) => Promise<JoinServerOutcome>;
42
71
  leaveServer: (args: {
43
72
  serverId: string;
44
73
  }) => Promise<void>;
@@ -54,6 +83,7 @@ export type GameRuntimeFeeds = {
54
83
  serverId: string;
55
84
  action: string;
56
85
  }, onChange: (view: GameRuntimeFeedView) => void) => FeedUnsubscribe;
86
+ requestServerBaseline?: (serverId: string) => void;
57
87
  };
58
88
  export type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = {
59
89
  transport: GameRuntimeTransport;
@@ -37,3 +37,5 @@ export declare function setChunk(snapshot: GameRuntimeSnapshot, chunkKey: string
37
37
  export declare function updateChunk(snapshot: GameRuntimeSnapshot, chunkKey: string, update: (chunk: RuntimeChunkRow) => RuntimeChunkRow): GameRuntimeSnapshot;
38
38
  /** Drop a chunk from a snapshot and mark the key dirty so the host deletes its stored row. */
39
39
  export declare function deleteChunk(snapshot: GameRuntimeSnapshot, chunkKey: string): GameRuntimeSnapshot;
40
+ /** Square chunk neighborhood around a world position, including its own chunk. */
41
+ export declare function chunkKeysAround(position: readonly [number, number, number], rings?: number, size?: number): string[];
@@ -81,3 +81,17 @@ export function deleteChunk(snapshot, chunkKey) {
81
81
  dirty: { ...snapshot.dirty, server: true, chunks: withChunkDirty(snapshot, chunkKey) },
82
82
  };
83
83
  }
84
+ /** Square chunk neighborhood around a world position, including its own chunk. */
85
+ export function chunkKeysAround(position, rings = 1, size = DEFAULT_CHUNK_SIZE) {
86
+ if (!Number.isSafeInteger(rings) || rings < 0 || rings > 128)
87
+ throw new RangeError("rings must be an integer from 0 to 128");
88
+ if (!Number.isFinite(size) || size <= 0 || !position.every(Number.isFinite))
89
+ throw new RangeError("Invalid chunk position or size");
90
+ const center = chunkCoordAt(position, size);
91
+ const keys = [];
92
+ for (let cx = center.cx - rings; cx <= center.cx + rings; cx++) {
93
+ for (let cz = center.cz - rings; cz <= center.cz + rings; cz++)
94
+ keys.push(chunkKeyOf({ cx, cz }));
95
+ }
96
+ return keys;
97
+ }
@@ -8,6 +8,11 @@ import type { SnapshotViewer, WorldSnapshot } from "./worldSnapshot.js";
8
8
  * growing a per-feature branch.
9
9
  */
10
10
  export interface ReplicationPolicy {
11
+ /** Decimal places used for replicated positions and angles. */
12
+ quantize?: {
13
+ position?: number;
14
+ angle?: number;
15
+ };
11
16
  /**
12
17
  * Replicate each player's private per-user state (inventory) only to that player — other viewers
13
18
  * receive none of it. Off by default so existing games keep broadcasting every player's state.
@@ -24,6 +29,10 @@ export interface ReplicationPolicy {
24
29
  * @internal host projection plumbing — games set ReplicationPolicy; the runtime applies it.
25
30
  */
26
31
  export declare function policyProjectsViewers(policy: ReplicationPolicy | undefined): boolean;
32
+ /** Quantize replicated entity poses according to a host projection policy.
33
+ * @internal host projection plumbing
34
+ */
35
+ export declare function quantizeWorldForReplication(world: WorldSnapshot, policy: ReplicationPolicy | undefined): WorldSnapshot;
27
36
  /**
28
37
  * Cull an entity list to a viewer's area of interest: keep the viewer's own entity plus every entity
29
38
  * within `radius` of it. When the viewer has no locatable entity the full list is returned (fail-open —
@@ -4,7 +4,23 @@
4
4
  export function policyProjectsViewers(policy) {
5
5
  if (policy === undefined)
6
6
  return false;
7
- return policy.privatePerUser === true || policy.aoiRadius !== undefined;
7
+ return policy.privatePerUser === true || policy.aoiRadius !== undefined || policy.quantize !== undefined;
8
+ }
9
+ /** Quantize replicated entity poses according to a host projection policy.
10
+ * @internal host projection plumbing
11
+ */
12
+ export function quantizeWorldForReplication(world, policy) {
13
+ if (policy?.quantize?.position === undefined && policy?.quantize?.angle === undefined)
14
+ return world;
15
+ const digits = (value, places) => places === undefined ? value : Number(value.toFixed(Math.max(0, places)));
16
+ const entities = world["entities"]?.map((entity) => ({
17
+ ...entity,
18
+ position: entity.position.map((value) => digits(value, policy.quantize?.position)),
19
+ rotationY: digits(entity.rotationY, policy.quantize?.angle),
20
+ rotationX: digits(entity.rotationX, policy.quantize?.angle),
21
+ rotationZ: digits(entity.rotationZ, policy.quantize?.angle),
22
+ }));
23
+ return entities === undefined ? world : { ...world, entities };
8
24
  }
9
25
  function distanceSquared(a, b) {
10
26
  const dx = a[0] - b[0];
@@ -1,6 +1,10 @@
1
1
  /** Who a host→client snapshot is being projected for — the identity a {@link SnapshotModule.project} filters against. */
2
2
  export interface SnapshotViewer {
3
3
  readonly userId: string;
4
+ /** A read-only connection role; spectators receive snapshots but cannot mutate the world. */
5
+ readonly role?: "player" | "spectator";
6
+ /** Host tick used by {@link SnapshotModule.priority} to throttle low-priority modules. */
7
+ readonly tick?: number;
4
8
  }
5
9
  /**
6
10
  * The replication seam for host-authoritative shared worlds: the opt-in feature manifest *is* the
@@ -37,6 +41,8 @@ export interface SnapshotModule<T = unknown> {
37
41
  project?(data: T, viewer: SnapshotViewer, world: WorldSnapshot): T;
38
42
  /** Monotone change counter; unchanged between host commits means this module didn't mutate and need not re-serialize. */
39
43
  version?(): number;
44
+ /** Return the host tick interval for this module; values greater than one omit it on intervening ticks. */
45
+ priority?(data: T, viewer: SnapshotViewer): number;
40
46
  }
41
47
  /** Full world baseline keyed by {@link SnapshotModule.key} — one entry per opted-in subsystem. */
42
48
  export type WorldSnapshot = Record<string, unknown>;