Files
OpenFrontIO/tests/TimelineSeekReplay.test.ts
scamiv a8ec56b5a4 feat(timeline): integrate timeline functionality into game client
- Added TimelineController to manage timeline events and state.
- Introduced TimelinePanel for user interaction with the timeline.
- Implemented LruCache for efficient storage of timeline records.
- Enhanced Transport and GameRenderer to support timeline features.
- Updated various layers to respond to timeline events, ensuring synchronization with game state.
- Added support for seeking and jumping within the timeline, improving user experience during gameplay.

This commit lays the groundwork for a more interactive and responsive timeline feature in the game client.
2026-02-20 20:11:51 +01:00

138 lines
3.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Config } from "../src/core/configuration/Config";
import { GameMapImpl } from "../src/core/game/GameMap";
import {
GameUpdateType,
type GameUpdateViewData,
} from "../src/core/game/GameUpdates";
import { GameView } from "../src/core/game/GameView";
import type { TerrainMapData } from "../src/core/game/TerrainMapLoader";
import type { WorkerClient } from "../src/core/worker/WorkerClient";
function createEmptyGameUpdates() {
const updates: any = {};
for (const v of Object.values(GameUpdateType)) {
if (typeof v === "number") {
updates[v] = [];
}
}
return updates;
}
function createView(w: number, h: number): GameView {
const terrain = new Uint8Array(w * h).fill(1 << 7);
const gameMap = new GameMapImpl(w, h, terrain, w * h);
const mapData: TerrainMapData = {
nations: [],
gameMap,
miniGameMap: gameMap,
};
return new GameView(
{} as unknown as WorkerClient,
{} as unknown as Config,
mapData,
"client1" as any,
"me",
"game1" as any,
[],
);
}
function packTileUpdate(tile: number, state16: number): bigint {
return (BigInt(tile) << 16n) | BigInt(state16 & 0xffff);
}
describe("Timeline-style seek via checkpoints + tick replay", () => {
it("reconstructs map state for arbitrary ticks", () => {
const w = 5;
const h = 4;
const totalTicks = 30;
const checkpointEvery = 5;
const baseline = createView(w, h);
const byTickState = new Map<number, Uint16Array>();
const checkpoints: {
tick: number;
checkpoint: ReturnType<GameView["exportCheckpoint"]>;
}[] = [{ tick: 0, checkpoint: baseline.exportCheckpoint() }];
const tickRecords: {
tick: number;
packedTileUpdatesBuffer: ArrayBuffer;
updates: any;
}[] = [];
for (let tick = 1; tick <= totalTicks; tick++) {
const tile1 = tick % (w * h);
const tile2 = (tick * 7) % (w * h);
const owner1 = (tick % 15) + 1;
const owner2 = ((tick + 3) % 15) + 1;
const fallout2 = tick % 2 === 0;
const state1 = owner1;
const state2 = owner2 | (fallout2 ? 1 << 13 : 0);
const packedTileUpdates = new BigUint64Array([
packTileUpdate(tile1, state1),
packTileUpdate(tile2, state2),
]);
const updates = createEmptyGameUpdates();
const gu: GameUpdateViewData = {
tick,
packedTileUpdates,
updates,
playerNameViewData: {},
};
baseline.update(gu);
byTickState.set(tick, baseline.exportCheckpoint().mapState);
tickRecords.push({
tick,
packedTileUpdatesBuffer: packedTileUpdates.buffer.slice(0),
updates,
});
if (tick % checkpointEvery === 0) {
checkpoints.push({ tick, checkpoint: baseline.exportCheckpoint() });
}
}
const nearestCheckpoint = (targetTick: number) => {
let best = checkpoints[0]?.checkpoint ?? baseline.exportCheckpoint();
for (const cp of checkpoints) {
if (cp.tick <= targetTick && cp.tick >= best.tick) {
best = cp.checkpoint;
}
}
return best;
};
const targets = [1, 2, 7, 13, 19, 24, 30];
for (const target of targets) {
const view = createView(w, h);
const cp = nearestCheckpoint(target);
view.importCheckpoint(cp);
for (const rec of tickRecords) {
if (rec.tick <= cp.tick) continue;
if (rec.tick > target) break;
view.update({
tick: rec.tick,
packedTileUpdates: new BigUint64Array(rec.packedTileUpdatesBuffer),
updates: rec.updates,
playerNameViewData: {},
});
}
const expected = byTickState.get(target);
expect(expected).toBeDefined();
expect(Array.from(view.exportCheckpoint().mapState)).toEqual(
Array.from(expected!),
);
}
});
});