@jgengine/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.d.ts +13 -0
- package/dist/ai/spawnDirector.js +41 -17
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +214 -129
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +52 -0
- package/dist/combat/shotOrigin.js +86 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +44 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +25 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +14 -1
- package/dist/game/lootTable.js +28 -7
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +116 -4
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +27 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +33 -3
- package/dist/input/axisInput.js +60 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +44 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/movementModel.d.ts +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +72 -8
- package/dist/runtime/gameContext.js +374 -61
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +18 -0
- package/dist/runtime/inputSnapshot.js +7 -0
- package/dist/runtime/motionIntents.d.ts +16 -6
- package/dist/runtime/motionIntents.js +31 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +50 -0
- package/dist/scene/colliders.js +101 -0
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +51 -5
- package/dist/scene/entityStore.js +119 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +17 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +212 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +21 -4
- package/dist/world/environmentSummary.js +22 -3
- package/dist/world/features.d.ts +185 -8
- package/dist/world/features.js +64 -6
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +69 -3
- package/dist/world/terrain.js +309 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +3013 -1726
- package/package.json +12 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
|
|
2
|
+
export const DEFAULT_VISIBILITY_SETTINGS = Object.freeze({
|
|
3
|
+
enabled: true,
|
|
4
|
+
streamingEnabled: true,
|
|
5
|
+
minRenderDistance: 0,
|
|
6
|
+
maxRenderDistance: Number.POSITIVE_INFINITY,
|
|
7
|
+
preloadMargin: 24,
|
|
8
|
+
unloadGraceMs: 5_000,
|
|
9
|
+
hysteresis: 8,
|
|
10
|
+
});
|
|
11
|
+
function squaredDistance(a, b) {
|
|
12
|
+
const dx = a.x - b.x;
|
|
13
|
+
const dy = a.y - b.y;
|
|
14
|
+
const dz = (a.z ?? 0) - (b.z ?? 0);
|
|
15
|
+
return dx * dx + dy * dy + dz * dz;
|
|
16
|
+
}
|
|
17
|
+
function intersectsRadius(camera, bounds, extraMargin) {
|
|
18
|
+
const radius = Math.max(0, camera.visibleRadius + extraMargin + bounds.radius);
|
|
19
|
+
return squaredDistance(camera.position, bounds.center) <= radius * radius;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Engine-level visibility and asset-residency policy.
|
|
23
|
+
*
|
|
24
|
+
* Render culling and asset streaming are intentionally separate. This class never
|
|
25
|
+
* destroys entities or throttles simulation; adapters decide how to apply results.
|
|
26
|
+
*/
|
|
27
|
+
export class VisibilitySystem {
|
|
28
|
+
settings;
|
|
29
|
+
#states = new Map();
|
|
30
|
+
constructor(settings = {}) {
|
|
31
|
+
this.settings = { ...DEFAULT_VISIBILITY_SETTINGS, ...settings };
|
|
32
|
+
}
|
|
33
|
+
evaluate(object, cameras, now = Date.now()) {
|
|
34
|
+
const overrides = object.overrides ?? {};
|
|
35
|
+
const state = this.#states.get(object.id) ?? { loaded: false, lastNeededAt: now };
|
|
36
|
+
if (overrides.alwaysVisible) {
|
|
37
|
+
this.#markNeeded(object.id, state, now);
|
|
38
|
+
return { render: true, load: true, unload: false, reason: "always-visible" };
|
|
39
|
+
}
|
|
40
|
+
if (!this.settings.enabled || overrides.cullingDisabled) {
|
|
41
|
+
this.#markNeeded(object.id, state, now);
|
|
42
|
+
return { render: true, load: true, unload: false, reason: "culling-disabled" };
|
|
43
|
+
}
|
|
44
|
+
const minDistance = Math.max(0, overrides.minRenderDistance ?? this.settings.minRenderDistance);
|
|
45
|
+
const maxDistance = Math.max(minDistance, overrides.maxRenderDistance ?? this.settings.maxRenderDistance);
|
|
46
|
+
const minDistanceSq = minDistance * minDistance;
|
|
47
|
+
const maxDistanceWithRadius = maxDistance + object.bounds.radius;
|
|
48
|
+
const maxDistanceSq = maxDistanceWithRadius * maxDistanceWithRadius;
|
|
49
|
+
let insideDistance = false;
|
|
50
|
+
let visible = false;
|
|
51
|
+
let preload = false;
|
|
52
|
+
for (const camera of cameras) {
|
|
53
|
+
const distanceSq = squaredDistance(camera.position, object.bounds.center);
|
|
54
|
+
if (distanceSq < minDistanceSq || distanceSq > maxDistanceSq)
|
|
55
|
+
continue;
|
|
56
|
+
insideDistance = true;
|
|
57
|
+
if (intersectsRadius(camera, object.bounds, this.settings.hysteresis)) {
|
|
58
|
+
visible = true;
|
|
59
|
+
}
|
|
60
|
+
if (camera.influencesStreaming !== false) {
|
|
61
|
+
const margin = overrides.preloadMargin ?? camera.preloadMargin ?? this.settings.preloadMargin;
|
|
62
|
+
if (intersectsRadius(camera, object.bounds, margin + this.settings.hysteresis)) {
|
|
63
|
+
preload = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (visible) {
|
|
68
|
+
this.#markNeeded(object.id, state, now);
|
|
69
|
+
return { render: true, load: true, unload: false, reason: "visible" };
|
|
70
|
+
}
|
|
71
|
+
if (overrides.streamingDisabled || !this.settings.streamingEnabled) {
|
|
72
|
+
state.loaded = true;
|
|
73
|
+
this.#states.set(object.id, state);
|
|
74
|
+
return {
|
|
75
|
+
render: false,
|
|
76
|
+
load: true,
|
|
77
|
+
unload: false,
|
|
78
|
+
reason: "streaming-disabled",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (preload) {
|
|
82
|
+
this.#markNeeded(object.id, state, now);
|
|
83
|
+
return { render: false, load: true, unload: false, reason: "preload" };
|
|
84
|
+
}
|
|
85
|
+
if (overrides.neverUnload) {
|
|
86
|
+
state.loaded = true;
|
|
87
|
+
this.#states.set(object.id, state);
|
|
88
|
+
return {
|
|
89
|
+
render: false,
|
|
90
|
+
load: true,
|
|
91
|
+
unload: false,
|
|
92
|
+
reason: insideDistance ? "outside-view" : "distance-culled",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const graceActive = state.loaded && now - state.lastNeededAt < this.settings.unloadGraceMs;
|
|
96
|
+
if (graceActive) {
|
|
97
|
+
this.#states.set(object.id, state);
|
|
98
|
+
return { render: false, load: true, unload: false, reason: "grace-period" };
|
|
99
|
+
}
|
|
100
|
+
state.loaded = false;
|
|
101
|
+
this.#states.set(object.id, state);
|
|
102
|
+
return {
|
|
103
|
+
render: false,
|
|
104
|
+
load: false,
|
|
105
|
+
unload: true,
|
|
106
|
+
reason: insideDistance ? "outside-view" : "distance-culled",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
setLoaded(id, loaded, now = Date.now()) {
|
|
110
|
+
const previous = this.#states.get(id);
|
|
111
|
+
this.#states.set(id, {
|
|
112
|
+
loaded,
|
|
113
|
+
lastNeededAt: loaded ? now : previous?.lastNeededAt ?? now,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
forget(id) {
|
|
117
|
+
this.#states.delete(id);
|
|
118
|
+
}
|
|
119
|
+
clear() {
|
|
120
|
+
this.#states.clear();
|
|
121
|
+
}
|
|
122
|
+
#markNeeded(id, state, now) {
|
|
123
|
+
state.loaded = true;
|
|
124
|
+
state.lastNeededAt = now;
|
|
125
|
+
this.#states.set(id, state);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { GameContext } from "./gameContext.js";
|
|
2
|
+
import type { InputFrame } from "./hostedGameRunner.js";
|
|
3
|
+
import type { HostedWorldSession } from "./hostedWorldSession.js";
|
|
4
|
+
import type { WorldDiff } from "./worldReplication.js";
|
|
5
|
+
import type { WorldSnapshot } from "./worldSnapshot.js";
|
|
6
|
+
/** Host→client frame: the full baseline a joiner needs, then per-tick diffs. What a transport marshals downstream. */
|
|
7
|
+
export type WorldServerFrame = {
|
|
8
|
+
t: "baseline";
|
|
9
|
+
revision: number;
|
|
10
|
+
snapshot: WorldSnapshot;
|
|
11
|
+
} | {
|
|
12
|
+
t: "diff";
|
|
13
|
+
diff: WorldDiff;
|
|
14
|
+
};
|
|
15
|
+
/** Client→host frame: a player's session verbs. What a transport marshals upstream. */
|
|
16
|
+
export type WorldClientFrame = {
|
|
17
|
+
t: "join";
|
|
18
|
+
isNew: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
t: "leave";
|
|
21
|
+
} | {
|
|
22
|
+
t: "command";
|
|
23
|
+
name: string;
|
|
24
|
+
input: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
t: "input";
|
|
27
|
+
frame: InputFrame;
|
|
28
|
+
};
|
|
29
|
+
/** One client's link on the host side: routes its upstream frames into the shared session, pushes it sync frames. */
|
|
30
|
+
export interface WorldHostConnection {
|
|
31
|
+
receive(frame: WorldClientFrame): void;
|
|
32
|
+
push(): void;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The transport-agnostic host: fans one {@link HostedWorldSession} out to many connections, each tracking its own
|
|
37
|
+
* revision cursor so a joiner gets a baseline and everyone else gets diffs. A ws server, a Convex function, or an
|
|
38
|
+
* in-process loopback all drive the same shape — decode a frame → `connection.receive`; after `session.tick` →
|
|
39
|
+
* `broadcast`. No wire format is assumed; frames are plain data a transport serializes however it likes.
|
|
40
|
+
*/
|
|
41
|
+
export interface WorldHost {
|
|
42
|
+
connect(userId: string, send: (frame: WorldServerFrame) => void): WorldHostConnection;
|
|
43
|
+
broadcast(): void;
|
|
44
|
+
session(): HostedWorldSession;
|
|
45
|
+
}
|
|
46
|
+
/** Build a {@link WorldHost} fanning one {@link HostedWorldSession} out to many cursor-tracked connections. */
|
|
47
|
+
export declare function createWorldHost(session: HostedWorldSession): WorldHost;
|
|
48
|
+
/** The client end of a {@link WorldHost} connection: applies server frames to a local `ctx`, sends session verbs upstream. */
|
|
49
|
+
export interface WorldClientLink {
|
|
50
|
+
receive(frame: WorldServerFrame): void;
|
|
51
|
+
join(isNew: boolean): void;
|
|
52
|
+
leave(): void;
|
|
53
|
+
command(name: string, input: unknown): void;
|
|
54
|
+
input(frame: InputFrame): void;
|
|
55
|
+
revision(): number;
|
|
56
|
+
}
|
|
57
|
+
/** Build a {@link WorldClientLink} — the client end that mirrors host frames into `ctx` and sends session verbs upstream. */
|
|
58
|
+
export declare function createWorldClientLink(ctx: Pick<GameContext, "hydrate">, send: (frame: WorldClientFrame) => void): WorldClientLink;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createWorldMirror } from "./worldMirror.js";
|
|
2
|
+
/** Build a {@link WorldHost} fanning one {@link HostedWorldSession} out to many cursor-tracked connections. */
|
|
3
|
+
export function createWorldHost(session) {
|
|
4
|
+
const connections = new Set();
|
|
5
|
+
function connect(userId, send) {
|
|
6
|
+
let cursor = 0;
|
|
7
|
+
const connection = {
|
|
8
|
+
receive(frame) {
|
|
9
|
+
switch (frame.t) {
|
|
10
|
+
case "join":
|
|
11
|
+
session.join(userId, frame.isNew);
|
|
12
|
+
break;
|
|
13
|
+
case "leave":
|
|
14
|
+
session.leave(userId);
|
|
15
|
+
break;
|
|
16
|
+
case "command":
|
|
17
|
+
session.command(userId, frame.name, frame.input);
|
|
18
|
+
break;
|
|
19
|
+
case "input":
|
|
20
|
+
session.input(userId, frame.frame);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
push() {
|
|
25
|
+
if (cursor !== 0 && cursor === session.revision())
|
|
26
|
+
return;
|
|
27
|
+
const sync = session.sync(cursor === 0 ? null : cursor);
|
|
28
|
+
if (sync.kind === "baseline") {
|
|
29
|
+
cursor = sync.revision;
|
|
30
|
+
send({ t: "baseline", revision: sync.revision, snapshot: sync.snapshot });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
cursor = sync.diff.revision;
|
|
34
|
+
send({ t: "diff", diff: sync.diff });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
close() {
|
|
38
|
+
connections.delete(connection);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
connections.add(connection);
|
|
42
|
+
return connection;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
connect,
|
|
46
|
+
broadcast() {
|
|
47
|
+
for (const connection of connections)
|
|
48
|
+
connection.push();
|
|
49
|
+
},
|
|
50
|
+
session: () => session,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Build a {@link WorldClientLink} — the client end that mirrors host frames into `ctx` and sends session verbs upstream. */
|
|
54
|
+
export function createWorldClientLink(ctx, send) {
|
|
55
|
+
const mirror = createWorldMirror(ctx);
|
|
56
|
+
return {
|
|
57
|
+
receive(frame) {
|
|
58
|
+
if (frame.t === "baseline")
|
|
59
|
+
mirror.applyBaseline(frame.revision, frame.snapshot);
|
|
60
|
+
else
|
|
61
|
+
mirror.applyDiff(frame.diff);
|
|
62
|
+
},
|
|
63
|
+
join: (isNew) => send({ t: "join", isNew }),
|
|
64
|
+
leave: () => send({ t: "leave" }),
|
|
65
|
+
command: (name, input) => send({ t: "command", name, input }),
|
|
66
|
+
input: (frame) => send({ t: "input", frame }),
|
|
67
|
+
revision: () => mirror.revision(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GameContext } from "./gameContext.js";
|
|
2
|
+
import type { HostedWorldSession } from "./hostedWorldSession.js";
|
|
3
|
+
import { type WorldDiff } from "./worldReplication.js";
|
|
4
|
+
import type { WorldSnapshot } from "./worldSnapshot.js";
|
|
5
|
+
/**
|
|
6
|
+
* The client end of host-authoritative replication: folds a host's baseline + {@link WorldDiff} stream onto a
|
|
7
|
+
* local {@link GameContext}. It keeps the last full {@link WorldSnapshot}, advances it with each diff, and pushes
|
|
8
|
+
* the result through `ctx.hydrate` — so the client mirrors exactly the subsystems its own game opted into and
|
|
9
|
+
* silently ignores host modules it lacks. This is the inverse of a {@link HostedWorldSession}; the transport in
|
|
10
|
+
* between (loopback, ws, Convex) is irrelevant.
|
|
11
|
+
*/
|
|
12
|
+
export interface WorldMirror {
|
|
13
|
+
applyBaseline(revision: number, snapshot: WorldSnapshot): void;
|
|
14
|
+
applyDiff(diff: WorldDiff): void;
|
|
15
|
+
revision(): number;
|
|
16
|
+
}
|
|
17
|
+
/** Build a {@link WorldMirror} that replicates a host's baseline + diff stream onto `ctx` via `ctx.hydrate`. */
|
|
18
|
+
export declare function createWorldMirror(ctx: Pick<GameContext, "hydrate">): WorldMirror;
|
|
19
|
+
/**
|
|
20
|
+
* Pull one replication step from a co-located {@link HostedWorldSession} into a {@link WorldMirror} — the
|
|
21
|
+
* no-network local path (host and client in one process). A fresh mirror pulls a baseline; thereafter it pulls
|
|
22
|
+
* a diff since its own revision. The same `sync(sinceRevision)` call is what a networked transport marshals.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pullWorld(session: HostedWorldSession, mirror: WorldMirror): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { applyWorldDiff } from "./worldReplication.js";
|
|
2
|
+
/** Build a {@link WorldMirror} that replicates a host's baseline + diff stream onto `ctx` via `ctx.hydrate`. */
|
|
3
|
+
export function createWorldMirror(ctx) {
|
|
4
|
+
let mirror = {};
|
|
5
|
+
let revision = 0;
|
|
6
|
+
return {
|
|
7
|
+
applyBaseline(nextRevision, snapshot) {
|
|
8
|
+
mirror = snapshot;
|
|
9
|
+
revision = nextRevision;
|
|
10
|
+
ctx.hydrate(snapshot);
|
|
11
|
+
},
|
|
12
|
+
applyDiff(diff) {
|
|
13
|
+
mirror = applyWorldDiff(mirror, diff);
|
|
14
|
+
revision = diff.revision;
|
|
15
|
+
ctx.hydrate(mirror);
|
|
16
|
+
},
|
|
17
|
+
revision: () => revision,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pull one replication step from a co-located {@link HostedWorldSession} into a {@link WorldMirror} — the
|
|
22
|
+
* no-network local path (host and client in one process). A fresh mirror pulls a baseline; thereafter it pulls
|
|
23
|
+
* a diff since its own revision. The same `sync(sinceRevision)` call is what a networked transport marshals.
|
|
24
|
+
*/
|
|
25
|
+
export function pullWorld(session, mirror) {
|
|
26
|
+
const sync = session.sync(mirror.revision() === 0 ? null : mirror.revision());
|
|
27
|
+
if (sync.kind === "baseline")
|
|
28
|
+
mirror.applyBaseline(sync.revision, sync.snapshot);
|
|
29
|
+
else
|
|
30
|
+
mirror.applyDiff(sync.diff);
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SceneEntity } from "../scene/entityStore.js";
|
|
2
|
+
import type { StatValueMap } from "../scene/entityStats.js";
|
|
3
|
+
import type { WorldSnapshot } from "./worldSnapshot.js";
|
|
4
|
+
/**
|
|
5
|
+
* A revision-stamped delta over a {@link WorldSnapshot}. The host sends one per tick to each client, carrying
|
|
6
|
+
* only what changed since that client's last acknowledged revision — entity/stat/store deltas plus whole
|
|
7
|
+
* snapshots of any other opted-in module (feed, leaderboard, chat, …) that changed. Fold it onto a prior
|
|
8
|
+
* baseline with {@link applyWorldDiff}.
|
|
9
|
+
*/
|
|
10
|
+
export interface WorldDiff {
|
|
11
|
+
revision: number;
|
|
12
|
+
entities: readonly SceneEntity[];
|
|
13
|
+
removedEntities: readonly string[];
|
|
14
|
+
stats: Record<string, StatValueMap>;
|
|
15
|
+
removedStats: readonly string[];
|
|
16
|
+
store: readonly (readonly [string, unknown])[];
|
|
17
|
+
removedStore: readonly string[];
|
|
18
|
+
/** Changed whole-module snapshots keyed by module name — everything a client can't reconstruct from deltas. */
|
|
19
|
+
modules: WorldSnapshot;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Turns successive full {@link WorldSnapshot}s into per-client {@link WorldDiff}s. Each `commit()` re-reads the
|
|
23
|
+
* world, stamps every item that changed with the new revision, and remembers removals; `diff(sinceRevision)`
|
|
24
|
+
* then replays exactly the items stamped after that revision. Everything the tracker holds is JSON — the same
|
|
25
|
+
* shape that rides the wire — so a diff is inherently serializable. Change-detection is a full re-serialize per
|
|
26
|
+
* commit; dirty-hint acceleration is a later optimization behind the same seam.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createWorldReplicator(takeSnapshot: () => WorldSnapshot): {
|
|
29
|
+
commit: () => number;
|
|
30
|
+
diff: (sinceRevision: number) => WorldDiff;
|
|
31
|
+
revision: () => number;
|
|
32
|
+
};
|
|
33
|
+
/** The stateful diff tracker returned by {@link createWorldReplicator}: `commit()`, `diff(sinceRevision)`, `revision()`. */
|
|
34
|
+
export type WorldReplicator = ReturnType<typeof createWorldReplicator>;
|
|
35
|
+
/**
|
|
36
|
+
* Diff two full {@link WorldSnapshot}s directly, stamping the result at `revision` — the stateless counterpart of
|
|
37
|
+
* {@link createWorldReplicator} for hosts that persist snapshots rather than keep a live tracker (Convex reconstructs
|
|
38
|
+
* per invocation). `applyWorldDiff(prev, diffSnapshots(prev, next, r))` reproduces `next`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function diffSnapshots(prev: WorldSnapshot, next: WorldSnapshot, revision: number): WorldDiff;
|
|
41
|
+
/**
|
|
42
|
+
* Fold a {@link WorldDiff} onto a prior {@link WorldSnapshot} baseline, returning the next full snapshot — the
|
|
43
|
+
* client-side inverse of {@link createWorldReplicator}. Pure data in, pure data out: upserts changed entities,
|
|
44
|
+
* stats and store keys, drops the removed ones, and replaces changed module snapshots wholesale.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyWorldDiff(baseline: WorldSnapshot, diff: WorldDiff): WorldSnapshot;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const CORE_KEYS = new Set(["entities", "stats", "store"]);
|
|
2
|
+
/**
|
|
3
|
+
* Turns successive full {@link WorldSnapshot}s into per-client {@link WorldDiff}s. Each `commit()` re-reads the
|
|
4
|
+
* world, stamps every item that changed with the new revision, and remembers removals; `diff(sinceRevision)`
|
|
5
|
+
* then replays exactly the items stamped after that revision. Everything the tracker holds is JSON — the same
|
|
6
|
+
* shape that rides the wire — so a diff is inherently serializable. Change-detection is a full re-serialize per
|
|
7
|
+
* commit; dirty-hint acceleration is a later optimization behind the same seam.
|
|
8
|
+
*/
|
|
9
|
+
export function createWorldReplicator(takeSnapshot) {
|
|
10
|
+
let revision = 0;
|
|
11
|
+
const entities = new Map();
|
|
12
|
+
const removedEntities = new Map();
|
|
13
|
+
const stats = new Map();
|
|
14
|
+
const removedStats = new Map();
|
|
15
|
+
const store = new Map();
|
|
16
|
+
const removedStore = new Map();
|
|
17
|
+
const modules = new Map();
|
|
18
|
+
function diffMap(live, tracked, removed, nextRevision) {
|
|
19
|
+
let changed = false;
|
|
20
|
+
for (const [key, json] of live) {
|
|
21
|
+
const prev = tracked.get(key);
|
|
22
|
+
if (prev === undefined || prev.json !== json) {
|
|
23
|
+
tracked.set(key, { json, revision: nextRevision });
|
|
24
|
+
removed.delete(key);
|
|
25
|
+
changed = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const key of tracked.keys()) {
|
|
29
|
+
if (!live.has(key)) {
|
|
30
|
+
tracked.delete(key);
|
|
31
|
+
removed.set(key, nextRevision);
|
|
32
|
+
changed = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return changed;
|
|
36
|
+
}
|
|
37
|
+
function commit() {
|
|
38
|
+
const snapshot = takeSnapshot();
|
|
39
|
+
const nextRevision = revision + 1;
|
|
40
|
+
const liveEntities = new Map();
|
|
41
|
+
for (const entity of (snapshot["entities"] ?? [])) {
|
|
42
|
+
liveEntities.set(entity.id, JSON.stringify(entity));
|
|
43
|
+
}
|
|
44
|
+
const liveStats = new Map();
|
|
45
|
+
for (const [id, value] of Object.entries((snapshot["stats"] ?? {}))) {
|
|
46
|
+
liveStats.set(id, JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
const liveStore = new Map();
|
|
49
|
+
for (const [key, value] of (snapshot["store"] ?? [])) {
|
|
50
|
+
liveStore.set(key, JSON.stringify(value ?? null));
|
|
51
|
+
}
|
|
52
|
+
const liveModules = new Map();
|
|
53
|
+
for (const [key, value] of Object.entries(snapshot)) {
|
|
54
|
+
if (CORE_KEYS.has(key))
|
|
55
|
+
continue;
|
|
56
|
+
liveModules.set(key, JSON.stringify(value ?? null));
|
|
57
|
+
}
|
|
58
|
+
let changed = diffMap(liveEntities, entities, removedEntities, nextRevision);
|
|
59
|
+
changed = diffMap(liveStats, stats, removedStats, nextRevision) || changed;
|
|
60
|
+
changed = diffMap(liveStore, store, removedStore, nextRevision) || changed;
|
|
61
|
+
changed = diffMap(liveModules, modules, new Map(), nextRevision) || changed;
|
|
62
|
+
if (changed)
|
|
63
|
+
revision = nextRevision;
|
|
64
|
+
return revision;
|
|
65
|
+
}
|
|
66
|
+
function diff(sinceRevision) {
|
|
67
|
+
const changedEntities = [];
|
|
68
|
+
for (const [, tracked] of entities) {
|
|
69
|
+
if (tracked.revision > sinceRevision)
|
|
70
|
+
changedEntities.push(JSON.parse(tracked.json));
|
|
71
|
+
}
|
|
72
|
+
const changedStats = {};
|
|
73
|
+
for (const [id, tracked] of stats) {
|
|
74
|
+
if (tracked.revision > sinceRevision)
|
|
75
|
+
changedStats[id] = JSON.parse(tracked.json);
|
|
76
|
+
}
|
|
77
|
+
const changedStore = [];
|
|
78
|
+
for (const [key, tracked] of store) {
|
|
79
|
+
if (tracked.revision > sinceRevision)
|
|
80
|
+
changedStore.push([key, JSON.parse(tracked.json)]);
|
|
81
|
+
}
|
|
82
|
+
const changedModules = {};
|
|
83
|
+
for (const [key, tracked] of modules) {
|
|
84
|
+
if (tracked.revision > sinceRevision)
|
|
85
|
+
changedModules[key] = JSON.parse(tracked.json);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
revision,
|
|
89
|
+
entities: changedEntities,
|
|
90
|
+
removedEntities: keysAfter(removedEntities, sinceRevision),
|
|
91
|
+
stats: changedStats,
|
|
92
|
+
removedStats: keysAfter(removedStats, sinceRevision),
|
|
93
|
+
store: changedStore,
|
|
94
|
+
removedStore: keysAfter(removedStore, sinceRevision),
|
|
95
|
+
modules: changedModules,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { commit, diff, revision: () => revision };
|
|
99
|
+
}
|
|
100
|
+
function keysAfter(map, sinceRevision) {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const [key, revision] of map) {
|
|
103
|
+
if (revision > sinceRevision)
|
|
104
|
+
out.push(key);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Diff two full {@link WorldSnapshot}s directly, stamping the result at `revision` — the stateless counterpart of
|
|
110
|
+
* {@link createWorldReplicator} for hosts that persist snapshots rather than keep a live tracker (Convex reconstructs
|
|
111
|
+
* per invocation). `applyWorldDiff(prev, diffSnapshots(prev, next, r))` reproduces `next`.
|
|
112
|
+
*/
|
|
113
|
+
export function diffSnapshots(prev, next, revision) {
|
|
114
|
+
const prevEntities = new Map();
|
|
115
|
+
for (const entity of (prev["entities"] ?? [])) {
|
|
116
|
+
prevEntities.set(entity.id, JSON.stringify(entity));
|
|
117
|
+
}
|
|
118
|
+
const entities = [];
|
|
119
|
+
const nextEntityIds = new Set();
|
|
120
|
+
for (const entity of (next["entities"] ?? [])) {
|
|
121
|
+
nextEntityIds.add(entity.id);
|
|
122
|
+
const json = JSON.stringify(entity);
|
|
123
|
+
if (prevEntities.get(entity.id) !== json)
|
|
124
|
+
entities.push(JSON.parse(json));
|
|
125
|
+
}
|
|
126
|
+
const removedEntities = [...prevEntities.keys()].filter((id) => !nextEntityIds.has(id));
|
|
127
|
+
const prevStats = (prev["stats"] ?? {});
|
|
128
|
+
const nextStats = (next["stats"] ?? {});
|
|
129
|
+
const stats = {};
|
|
130
|
+
for (const [id, value] of Object.entries(nextStats)) {
|
|
131
|
+
const json = JSON.stringify(value);
|
|
132
|
+
if (JSON.stringify(prevStats[id]) !== json)
|
|
133
|
+
stats[id] = JSON.parse(json);
|
|
134
|
+
}
|
|
135
|
+
const removedStats = Object.keys(prevStats).filter((id) => !(id in nextStats));
|
|
136
|
+
const prevStore = new Map((prev["store"] ?? []));
|
|
137
|
+
const nextStore = new Map((next["store"] ?? []));
|
|
138
|
+
const store = [];
|
|
139
|
+
for (const [key, value] of nextStore) {
|
|
140
|
+
const json = JSON.stringify(value ?? null);
|
|
141
|
+
if (JSON.stringify(prevStore.get(key) ?? null) !== json)
|
|
142
|
+
store.push([key, JSON.parse(json)]);
|
|
143
|
+
}
|
|
144
|
+
const removedStore = [...prevStore.keys()].filter((key) => !nextStore.has(key));
|
|
145
|
+
const modules = {};
|
|
146
|
+
for (const [key, value] of Object.entries(next)) {
|
|
147
|
+
if (CORE_KEYS.has(key))
|
|
148
|
+
continue;
|
|
149
|
+
const json = JSON.stringify(value ?? null);
|
|
150
|
+
if (JSON.stringify(prev[key] ?? null) !== json)
|
|
151
|
+
modules[key] = JSON.parse(json);
|
|
152
|
+
}
|
|
153
|
+
return { revision, entities, removedEntities, stats, removedStats, store, removedStore, modules };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Fold a {@link WorldDiff} onto a prior {@link WorldSnapshot} baseline, returning the next full snapshot — the
|
|
157
|
+
* client-side inverse of {@link createWorldReplicator}. Pure data in, pure data out: upserts changed entities,
|
|
158
|
+
* stats and store keys, drops the removed ones, and replaces changed module snapshots wholesale.
|
|
159
|
+
*/
|
|
160
|
+
export function applyWorldDiff(baseline, diff) {
|
|
161
|
+
const next = { ...baseline };
|
|
162
|
+
const entityById = new Map();
|
|
163
|
+
for (const entity of (baseline["entities"] ?? []))
|
|
164
|
+
entityById.set(entity.id, entity);
|
|
165
|
+
for (const entity of diff.entities)
|
|
166
|
+
entityById.set(entity.id, entity);
|
|
167
|
+
for (const id of diff.removedEntities)
|
|
168
|
+
entityById.delete(id);
|
|
169
|
+
next["entities"] = Array.from(entityById.values());
|
|
170
|
+
const statsById = { ...(baseline["stats"] ?? {}) };
|
|
171
|
+
for (const [id, value] of Object.entries(diff.stats))
|
|
172
|
+
statsById[id] = value;
|
|
173
|
+
for (const id of diff.removedStats)
|
|
174
|
+
delete statsById[id];
|
|
175
|
+
next["stats"] = statsById;
|
|
176
|
+
const storeByKey = new Map((baseline["store"] ?? []));
|
|
177
|
+
for (const [key, value] of diff.store)
|
|
178
|
+
storeByKey.set(key, value);
|
|
179
|
+
for (const key of diff.removedStore)
|
|
180
|
+
storeByKey.delete(key);
|
|
181
|
+
next["store"] = Array.from(storeByKey.entries());
|
|
182
|
+
for (const [key, value] of Object.entries(diff.modules))
|
|
183
|
+
next[key] = value;
|
|
184
|
+
return next;
|
|
185
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replication seam for host-authoritative shared worlds: the opt-in feature manifest *is* the
|
|
3
|
+
* replication schema. Each live subsystem a game opts into registers a {@link SnapshotModule} keyed
|
|
4
|
+
* by name; the host serializes exactly the registered set into a {@link WorldSnapshot} and a client
|
|
5
|
+
* hydrates the same keys back. Adding a replicated subsystem is a registration, never a new branch.
|
|
6
|
+
*/
|
|
7
|
+
export interface SnapshotModule<T = unknown> {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
snapshot(): T;
|
|
10
|
+
hydrate(data: T): void;
|
|
11
|
+
}
|
|
12
|
+
/** Full world baseline keyed by {@link SnapshotModule.key} — one entry per opted-in subsystem. */
|
|
13
|
+
export type WorldSnapshot = Record<string, unknown>;
|
|
14
|
+
/** Serialize every registered module into one keyed baseline — the host→client full-world send. */
|
|
15
|
+
export declare function composeWorldSnapshot(modules: readonly SnapshotModule[]): WorldSnapshot;
|
|
16
|
+
/** Hydrate every registered module whose key is present in `snapshot`; keys absent from it are left untouched. */
|
|
17
|
+
export declare function applyWorldSnapshot(modules: readonly SnapshotModule[], snapshot: WorldSnapshot): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Serialize every registered module into one keyed baseline — the host→client full-world send. */
|
|
2
|
+
export function composeWorldSnapshot(modules) {
|
|
3
|
+
const snapshot = {};
|
|
4
|
+
for (const module of modules)
|
|
5
|
+
snapshot[module.key] = module.snapshot();
|
|
6
|
+
return snapshot;
|
|
7
|
+
}
|
|
8
|
+
/** Hydrate every registered module whose key is present in `snapshot`; keys absent from it are left untouched. */
|
|
9
|
+
export function applyWorldSnapshot(modules, snapshot) {
|
|
10
|
+
for (const module of modules) {
|
|
11
|
+
if (Object.prototype.hasOwnProperty.call(snapshot, module.key)) {
|
|
12
|
+
module.hydrate(snapshot[module.key]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
export type ColliderPurpose = "physical" | "damage";
|
|
3
|
+
export type ColliderShape = {
|
|
4
|
+
kind: "sphere";
|
|
5
|
+
radius: number;
|
|
6
|
+
offset?: EntityPosition;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "aabb";
|
|
9
|
+
halfExtents: EntityPosition;
|
|
10
|
+
offset?: EntityPosition;
|
|
11
|
+
};
|
|
12
|
+
export interface ColliderDef {
|
|
13
|
+
name: string;
|
|
14
|
+
purpose: ColliderPurpose;
|
|
15
|
+
shape: ColliderShape;
|
|
16
|
+
/** Defaults true for `purpose: "damage"`, false for `purpose: "physical"`. */
|
|
17
|
+
damageEligible?: boolean;
|
|
18
|
+
/** Defaults true — a blocking collider stops a ray at first impact. */
|
|
19
|
+
blocks?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface EntityColliderSet {
|
|
22
|
+
/** Single physical collision body (push / obstruction). */
|
|
23
|
+
body?: ColliderDef;
|
|
24
|
+
/** Named damage hitboxes (zero or more). */
|
|
25
|
+
hitboxes?: readonly ColliderDef[];
|
|
26
|
+
}
|
|
27
|
+
export interface ResolvedCollider {
|
|
28
|
+
name: string;
|
|
29
|
+
purpose: ColliderPurpose;
|
|
30
|
+
shape: ColliderShape;
|
|
31
|
+
damageEligible: boolean;
|
|
32
|
+
blocks: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
|
|
35
|
+
export declare const DEFAULT_OBJECT_HALF_EXTENTS: EntityPosition;
|
|
36
|
+
/** Matches the shell's fallback actor capsule (~0.7m wide, feet at y=0 to head at ~1.8m). */
|
|
37
|
+
export declare const DEFAULT_ENTITY_BODY_HALF_EXTENTS: EntityPosition;
|
|
38
|
+
/** Entity-local center of the default body hitbox (half its height above the feet). */
|
|
39
|
+
export declare const DEFAULT_ENTITY_BODY_OFFSET: EntityPosition;
|
|
40
|
+
export declare function defaultEntityColliders(): EntityColliderSet;
|
|
41
|
+
export declare function defaultObjectColliders(halfExtents?: EntityPosition): EntityColliderSet;
|
|
42
|
+
/** Blocking physical body derived from an object's rendered scale: a grounded box spanning the visual (base at y=0, matching the shell's fallback mesh). */
|
|
43
|
+
export declare function scaledObjectColliders(scale: readonly [number, number, number]): EntityColliderSet;
|
|
44
|
+
export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
|
|
45
|
+
export declare function worldOffset(local: EntityPosition | undefined, position: EntityPosition, rotationY: number): EntityPosition;
|
|
46
|
+
export declare function colliderWorldCenter(collider: ResolvedCollider, position: EntityPosition, rotationY: number): EntityPosition;
|
|
47
|
+
export declare function colliderBounds(collider: ResolvedCollider, position: EntityPosition, rotationY: number): {
|
|
48
|
+
min: EntityPosition;
|
|
49
|
+
max: EntityPosition;
|
|
50
|
+
};
|