mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-30 22:13:24 +00:00
a8ec56b5a4
- 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.
138 lines
3.8 KiB
TypeScript
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!),
|
|
);
|
|
}
|
|
});
|
|
});
|