decla-red/frontend/src/scripts/helper/server-timeline.ts
2026-08-22 18:35:53 +01:00

70 lines
1.8 KiB
TypeScript

import { clamp, settings } from 'shared';
const rateGain = 2;
const maxRateAdjustment = 0.25;
const resyncSeconds = 0.3;
/**
* Playback clock for streamed server state — renders interpolationDelaySeconds
* behind the server clock so a newer snapshot is usually available; the cursor
* adjusts its rate gently and only snaps after a long divergence.
*/
class ServerTimeline {
private cursor?: number;
private newestSnapshotTime?: number;
private sinceNewestSnapshot = 0;
private _snapshotTime = 0;
public get snapshotTime(): number {
return this._snapshotTime;
}
public get renderTime(): number {
return this.cursor ?? 0;
}
public onSnapshot(timestamp: number) {
this._snapshotTime = timestamp;
if (this.newestSnapshotTime === undefined || timestamp > this.newestSnapshotTime) {
this.newestSnapshotTime = timestamp;
this.sinceNewestSnapshot = 0;
}
}
// Must be called with unscaled wall-clock time, even during the end-game
// slow motion: the slowdown is already baked into the snapshots.
public step(deltaTimeInSeconds: number) {
if (this.newestSnapshotTime === undefined) {
return;
}
this.sinceNewestSnapshot += deltaTimeInSeconds;
const target =
this.newestSnapshotTime +
this.sinceNewestSnapshot -
settings.interpolationDelaySeconds;
if (this.cursor === undefined || Math.abs(target - this.cursor) > resyncSeconds) {
this.cursor = target;
return;
}
const rate = clamp(
1 + (target - this.cursor) * rateGain,
1 - maxRateAdjustment,
1 + maxRateAdjustment,
);
this.cursor += deltaTimeInSeconds * rate;
}
public reset() {
this.cursor = undefined;
this.newestSnapshotTime = undefined;
this.sinceNewestSnapshot = 0;
this._snapshotTime = 0;
}
}
export const serverTimeline = new ServerTimeline();