@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.
- package/CHANGELOG.md +130 -2
- package/dist/ai/decisionGraph.d.ts +52 -0
- package/dist/ai/decisionGraph.js +82 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/anim/animGraph.d.ts +144 -0
- package/dist/anim/animGraph.js +286 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +26 -0
- package/dist/anim/locomotionGraph.js +33 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +10 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +89 -0
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +16 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +16 -0
- package/dist/game/cameraConfig.d.ts +8 -0
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +8 -1
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +97 -3
- package/dist/game/systemRuntime.js +6 -1
- package/dist/input/actionBindings.d.ts +2 -1
- package/dist/input/actionBindings.js +10 -8
- package/dist/input/actionContexts.d.ts +21 -0
- package/dist/input/actionContexts.js +56 -0
- package/dist/input/gamepadModel.d.ts +45 -0
- package/dist/input/gamepadModel.js +69 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +103 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- package/dist/movement/playerMovement.d.ts +10 -2
- package/dist/movement/playerMovement.js +218 -4
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +30 -7
- package/dist/movement/solidObstacles.js +27 -4
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navMesh.d.ts +33 -0
- package/dist/nav/navMesh.js +164 -0
- package/dist/physics/flightDynamics.d.ts +8 -0
- package/dist/physics/kinematicVehicle.d.ts +6 -1
- package/dist/physics/physicsBackend.d.ts +191 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +117 -0
- package/dist/physics/physicsWorldBackend.d.ts +44 -0
- package/dist/physics/physicsWorldBackend.js +485 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +96 -0
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +11 -5
- package/dist/render/lookPreset.js +29 -4
- package/dist/render/postProcessing.d.ts +10 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/commandInput.d.ts +15 -0
- package/dist/runtime/commandInput.js +13 -0
- package/dist/runtime/commandRunner.d.ts +44 -3
- package/dist/runtime/commandRunner.js +41 -3
- package/dist/runtime/gameContext.js +11 -1
- package/dist/runtime/gameContextTypes.d.ts +14 -4
- package/dist/runtime/gameRuntime.d.ts +33 -4
- package/dist/runtime/gameRuntime.js +26 -5
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +8 -0
- package/dist/runtime/inputSnapshot.js +1 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +38 -2
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +19 -1
- package/dist/scene/behaviorRuntime.js +55 -4
- package/dist/scene/behaviors.d.ts +8 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/sensor/perception.d.ts +60 -0
- package/dist/sensor/perception.js +142 -0
- package/dist/settings/graphicsProfile.d.ts +19 -0
- package/dist/settings/graphicsProfile.js +36 -0
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/world/buildings.d.ts +12 -0
- package/dist/world/buildings.js +43 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +132 -0
- package/dist/world/cityKind.js +315 -15
- package/dist/world/features.d.ts +27 -1
- package/dist/world/features.js +7 -2
- package/dist/world/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +98 -22
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +40 -18
- package/skills/jgengine-gameplay/capabilities.md +12 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const TWO_PI = Math.PI * 2;
|
|
2
|
+
/** Shortest-arc interpolation between two yaw angles. */
|
|
3
|
+
export function lerpAngle(from, to, t) {
|
|
4
|
+
let delta = (to - from) % TWO_PI;
|
|
5
|
+
if (delta > Math.PI)
|
|
6
|
+
delta -= TWO_PI;
|
|
7
|
+
else if (delta < -Math.PI)
|
|
8
|
+
delta += TWO_PI;
|
|
9
|
+
return from + delta * t;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Previous-step pose buffer for render interpolation under a fixed-step loop. Reuses one 4-slot array per
|
|
13
|
+
* entity so a step over N entities allocates only for entities seen for the first time.
|
|
14
|
+
*
|
|
15
|
+
* @capability pose-interpolation render entity poses between fixed simulation steps without stutter
|
|
16
|
+
*/
|
|
17
|
+
export function createPoseHistory(options) {
|
|
18
|
+
let snapDistance = options?.snapDistance ?? 8;
|
|
19
|
+
const previous = new Map();
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
return {
|
|
22
|
+
beginStep(entities) {
|
|
23
|
+
seen.clear();
|
|
24
|
+
for (const entity of entities.list()) {
|
|
25
|
+
seen.add(entity.id);
|
|
26
|
+
let slot = previous.get(entity.id);
|
|
27
|
+
if (slot === undefined) {
|
|
28
|
+
slot = [0, 0, 0, 0];
|
|
29
|
+
previous.set(entity.id, slot);
|
|
30
|
+
}
|
|
31
|
+
slot[0] = entity.position[0];
|
|
32
|
+
slot[1] = entity.position[1];
|
|
33
|
+
slot[2] = entity.position[2];
|
|
34
|
+
slot[3] = entity.rotationY;
|
|
35
|
+
}
|
|
36
|
+
if (previous.size !== seen.size) {
|
|
37
|
+
for (const id of previous.keys()) {
|
|
38
|
+
if (!seen.has(id))
|
|
39
|
+
previous.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
sample(entities, id, alpha, out) {
|
|
44
|
+
const live = entities.get(id);
|
|
45
|
+
if (live === null)
|
|
46
|
+
return false;
|
|
47
|
+
const prev = previous.get(id);
|
|
48
|
+
const t = alpha < 0 ? 0 : alpha > 1 ? 1 : alpha;
|
|
49
|
+
if (prev === undefined || t >= 1) {
|
|
50
|
+
out[0] = live.position[0];
|
|
51
|
+
out[1] = live.position[1];
|
|
52
|
+
out[2] = live.position[2];
|
|
53
|
+
out[3] = live.rotationY;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const dx = live.position[0] - prev[0];
|
|
57
|
+
const dy = live.position[1] - prev[1];
|
|
58
|
+
const dz = live.position[2] - prev[2];
|
|
59
|
+
if (dx * dx + dy * dy + dz * dz > snapDistance * snapDistance) {
|
|
60
|
+
out[0] = live.position[0];
|
|
61
|
+
out[1] = live.position[1];
|
|
62
|
+
out[2] = live.position[2];
|
|
63
|
+
out[3] = live.rotationY;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
out[0] = prev[0] + dx * t;
|
|
67
|
+
out[1] = prev[1] + dy * t;
|
|
68
|
+
out[2] = prev[2] + dz * t;
|
|
69
|
+
out[3] = lerpAngle(prev[3], live.rotationY, t);
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
snap(id) {
|
|
73
|
+
previous.delete(id);
|
|
74
|
+
},
|
|
75
|
+
retune(next) {
|
|
76
|
+
if (next.snapDistance !== undefined)
|
|
77
|
+
snapDistance = next.snapDistance;
|
|
78
|
+
},
|
|
79
|
+
snapshot() {
|
|
80
|
+
const copy = {};
|
|
81
|
+
for (const [id, pose] of previous)
|
|
82
|
+
copy[id] = [pose[0], pose[1], pose[2], pose[3]];
|
|
83
|
+
return { previous: copy };
|
|
84
|
+
},
|
|
85
|
+
restore(state) {
|
|
86
|
+
previous.clear();
|
|
87
|
+
for (const [id, pose] of Object.entries(state.previous))
|
|
88
|
+
previous.set(id, [pose[0], pose[1], pose[2], pose[3]]);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { InputFrame } from "./inputSnapshot.js";
|
|
2
|
+
/** Deterministic state transition used to advance one predicted input frame. */
|
|
3
|
+
export type PredictionStep<S> = (state: S, input: InputFrame, dt: number) => S;
|
|
4
|
+
/** One input, tick, and resulting state retained for authoritative replay. */
|
|
5
|
+
export type PredictionRecord<S> = {
|
|
6
|
+
tick: number;
|
|
7
|
+
input: InputFrame;
|
|
8
|
+
state: S;
|
|
9
|
+
dt: number;
|
|
10
|
+
};
|
|
11
|
+
/** Serializable prediction state containing the confirmation baseline and pending input history. */
|
|
12
|
+
export type PredictionSnapshot<S> = {
|
|
13
|
+
confirmedTick: number;
|
|
14
|
+
confirmed: S;
|
|
15
|
+
predicted: S;
|
|
16
|
+
records: PredictionRecord<S>[];
|
|
17
|
+
};
|
|
18
|
+
/** Stateful prediction handle supporting recording, reconciliation, inspection, and restoration. */
|
|
19
|
+
export type PredictionBuffer<S> = {
|
|
20
|
+
record(input: InputFrame, dt?: number, observedState?: S): S;
|
|
21
|
+
reconcile(tick: number, authoritative: S): {
|
|
22
|
+
error: number;
|
|
23
|
+
replayed: number;
|
|
24
|
+
snapped: boolean;
|
|
25
|
+
state: S;
|
|
26
|
+
};
|
|
27
|
+
state(): S;
|
|
28
|
+
snapshot(): PredictionSnapshot<S>;
|
|
29
|
+
restore(next: PredictionSnapshot<S>): void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Keeps local input predictions ahead of the last server confirmation. Reconciliation replaces the
|
|
33
|
+
* confirmed state and deterministically replays unconfirmed inputs through the supplied step function.
|
|
34
|
+
* `maxTicks` bounds retained work and memory; dropped inputs are folded into the confirmed baseline.
|
|
35
|
+
* @capability client-side input prediction with replay reconciliation
|
|
36
|
+
*/
|
|
37
|
+
export declare function createPredictionBuffer<S>(config: {
|
|
38
|
+
initial: S;
|
|
39
|
+
step: PredictionStep<S>;
|
|
40
|
+
maxTicks: number;
|
|
41
|
+
startTick?: number;
|
|
42
|
+
dt?: number;
|
|
43
|
+
/** Error above which a caller should snap its rendered pose to the reconciled result. */
|
|
44
|
+
snapThreshold?: number;
|
|
45
|
+
}): PredictionBuffer<S>;
|
|
@@ -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
|
+
}
|