Files
healer-man/src/game/dungeonSimulationStore.test.ts
T
2026-09-16 20:00:13 -04:00

88 lines
3.5 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import {
DUNGEON_SIMULATION_FIXED_STEP_MS,
fixedSimulationTickCount,
useDungeonSimulationStore,
} from "./dungeonSimulationStore";
import { createSpectatorDungeonParty } from "./partyStore";
import { routeHasExactApproval } from "./dungeonRoutes";
import type { DungeonRouteDefinitionV1, DungeonSimulationSpeed } from "./dungeonSimulationTypes";
const route: DungeonRouteDefinitionV1 = {
schemaVersion: 1,
id: "primary",
dungeonId: "test-dungeon",
mapId: 1,
hash: "a".repeat(64),
generationStatus: "Candidate",
generationIssues: [],
legs: [{
id: "leg-1",
objectiveId: "boss-1",
objectiveName: "Boss",
checkpoints: [[0, 0, 0], [4, 0, 0]],
expectedDistance: 4,
corridorPlanarDistance: 10,
corridorVerticalDistance: 4,
actions: [],
}],
};
describe("dungeon simulation fixed clock", () => {
beforeEach(() => useDungeonSimulationStore.getState().leave());
it("produces the same ordered ticks at every playback speed", () => {
const speeds: readonly DungeonSimulationSpeed[] = [0.5, 1, 2, 4];
for (const speed of speeds) {
const wallDuration = 500 / speed;
const result = fixedSimulationTickCount(0, wallDuration, speed);
expect(result.ticks).toBe(500 / DUNGEON_SIMULATION_FIXED_STEP_MS);
expect(result.accumulatorMs).toBe(0);
}
});
it("freezes while paused and advances exactly one tick per step", () => {
const store = useDungeonSimulationStore.getState();
store.startLive({ dungeonId: route.dungeonId, route, mode: "navigation-only", seed: "clock", startedAt: 10_000 });
store.setRunning(false);
expect(useDungeonSimulationStore.getState().consumeTicks(5_000)).toEqual([]);
expect(useDungeonSimulationStore.getState().elapsedMs).toBe(0);
useDungeonSimulationStore.getState().step();
expect(useDungeonSimulationStore.getState().consumeTicks(0)).toEqual([10_050]);
expect(useDungeonSimulationStore.getState().elapsedMs).toBe(50);
});
});
describe("spectator roster and exact-hash approvals", () => {
it("tracks the chosen party member and returns to follow mode when one is picked", () => {
const store = useDungeonSimulationStore.getState();
store.startLive({ dungeonId: route.dungeonId, route, mode: "navigation-only", seed: "camera" });
expect(useDungeonSimulationStore.getState()).toMatchObject({
cameraMode: "follow-member",
spectatedMemberId: null,
});
store.setCameraMode("free-fly");
store.setSpectatedMember("party-healer");
expect(useDungeonSimulationStore.getState()).toMatchObject({
cameraMode: "follow-member",
spectatedMemberId: "party-healer",
});
store.leave();
expect(useDungeonSimulationStore.getState().spectatedMemberId).toBeNull();
});
it("creates a stable tank, healer, and three damage dealers", () => {
const first = createSpectatorDungeonParty("same-seed", 80, "wow");
const second = createSpectatorDungeonParty("same-seed", 80, "wow");
expect(second).toEqual(first);
expect(first.map((member) => member.role)).toEqual(["tank", "healer", "damage", "damage", "damage"]);
});
it("rejects stale hashes and generator-rejected routes", () => {
const approval = { dungeonId: route.dungeonId, routeId: route.id, routeHash: route.hash, reviewedAt: 1 };
expect(routeHasExactApproval(route, [approval])).toBe(true);
expect(routeHasExactApproval({ ...route, hash: "b".repeat(64) }, [approval])).toBe(false);
expect(routeHasExactApproval({ ...route, generationStatus: "Rejected" }, [approval])).toBe(false);
});
});