109 lines
7.1 KiB
TypeScript
109 lines
7.1 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { DUNGEON_DEFINITIONS, requireDungeonDefinition } from "./dungeonRegistry";
|
|
import { completedDungeonRun, finalDungeonBoss, useDungeonRunStore } from "./dungeonRunStore";
|
|
import { normalizeDungeonRunProgress, formatDungeonDuration, type DungeonRunMember } from "./dungeonRunTypes";
|
|
import { activateDungeonEncounterDefinitions, activeDungeonCompletedObjectiveIds, activeDungeonRunIsComplete,
|
|
advanceDungeonEncounters, createDungeonEncounterSnapshot, exportDungeonEncounterCheckpointSnapshots,
|
|
importDungeonEncounterSnapshots, reduceDungeonEncounter, resetDungeonEncounters } from "./dungeonEncounterRuntime";
|
|
|
|
const party: readonly DungeonRunMember[] = [{ name: "Tester", classId: "rom-priest", level: 1, role: "healer", npc: false }];
|
|
beforeEach(() => { useDungeonRunStore.setState({ progress: null }); });
|
|
|
|
describe("dungeon completion and run timing", () => {
|
|
it("preserves the selected difficulty on resume and starts a new timer when it changes", () => {
|
|
const run = useDungeonRunStore.getState();
|
|
run.begin("deadmines", "normal", undefined, false, 7); run.start(1000, party);
|
|
const saved = normalizeDungeonRunProgress(JSON.parse(JSON.stringify(useDungeonRunStore.getState().progress)))!;
|
|
run.begin("deadmines", "normal", saved, false, 7);
|
|
expect(useDungeonRunStore.getState().progress).toEqual(saved);
|
|
run.begin("deadmines", "normal", saved, false, 8);
|
|
expect(useDungeonRunStore.getState().progress).toMatchObject({ bossDifficulty: 8, startedAt: null });
|
|
expect(useDungeonRunStore.getState().progress!.id).not.toBe(saved.id);
|
|
expect(normalizeDungeonRunProgress({ ...saved, bossDifficulty: 11 })).toBeNull();
|
|
expect(normalizeDungeonRunProgress({ ...saved, bossDifficulty: 1.5 })).toBeNull();
|
|
});
|
|
it("completes Barren Caves when Anglo dies, records its real spawn id, and survives checkpoint hydration", () => {
|
|
const dungeon = requireDungeonDefinition("barren-caves");
|
|
activateDungeonEncounterDefinitions(dungeon.id, dungeon.encounters ?? [], "normal");
|
|
resetDungeonEncounters("dungeon", dungeon.id);
|
|
const run = useDungeonRunStore.getState();
|
|
run.begin(dungeon.id, "normal"); run.start(1000, party);
|
|
advanceDungeonEncounters(dungeon.id, { kind: "tick", now: 1000, facts: { registeredEntityIds: [], deadEntityIds: [] } });
|
|
expect(activeDungeonRunIsComplete(dungeon.id)).toBe(false);
|
|
const boss = finalDungeonBoss(dungeon)!;
|
|
expect(boss.name).toBe("Anglo");
|
|
advanceDungeonEncounters(dungeon.id, { kind: "tick", now: 12456,
|
|
facts: { registeredEntityIds: [boss.id], deadEntityIds: [boss.id] } });
|
|
run.advance(activeDungeonCompletedObjectiveIds(dungeon.id), [boss.id], 12456, party);
|
|
expect(activeDungeonRunIsComplete(dungeon.id)).toBe(true);
|
|
expect(completedDungeonRun(useDungeonRunStore.getState().progress!)).toMatchObject({
|
|
durationMs: 11456, fullClear: true, killedBossIds: [boss.id], party,
|
|
});
|
|
const snapshots = exportDungeonEncounterCheckpointSnapshots(dungeon.id, 0);
|
|
resetDungeonEncounters("dungeon", dungeon.id);
|
|
importDungeonEncounterSnapshots(dungeon.id, snapshots, { authoritative: true });
|
|
expect(activeDungeonRunIsComplete(dungeon.id)).toBe(true);
|
|
const result = useDungeonRunStore.getState().progress;
|
|
run.advance([boss.id], [boss.id], 99999, []); run.start(99999, []);
|
|
expect(useDungeonRunStore.getState().progress).toBe(result);
|
|
});
|
|
|
|
it("adds durable completion bookkeeping to every RoM dungeon with configured bosses, without awarding missing bosses", () => {
|
|
for (const dungeon of DUNGEON_DEFINITIONS.filter((entry) => entry.expansion === "runewaker" && entry.bosses.length > 0)) {
|
|
const baseline = dungeon.encounters!.find((entry) => entry.id === `${dungeon.id}:completion-baseline`)!;
|
|
expect(baseline, dungeon.id).toBeDefined();
|
|
let snapshot = createDungeonEncounterSnapshot(baseline, 0);
|
|
snapshot = reduceDungeonEncounter(baseline, snapshot, { kind: "tick", now: 10,
|
|
facts: { registeredEntityIds: [], deadEntityIds: [] } }).snapshot;
|
|
expect(snapshot.flags["dungeon-complete"], dungeon.id).toBe(false);
|
|
const ordinary = [...new Set([...dungeon.objectiveOrder, ...dungeon.bosses]
|
|
.filter((boss) => !boss.eventOnly).map((boss) => boss.id))];
|
|
snapshot = reduceDungeonEncounter(baseline, snapshot, { kind: "tick", now: 20,
|
|
facts: { registeredEntityIds: ordinary, deadEntityIds: ordinary } }).snapshot;
|
|
expect(snapshot.completedObjectiveIds, dungeon.id).toEqual(expect.arrayContaining(ordinary));
|
|
if (!dungeon.objectiveOrder.some((boss) => !boss.optional && boss.eventOnly)) {
|
|
expect(snapshot.flags["dungeon-complete"], dungeon.id).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("finishes a final boss clear even when earlier bosses were skipped, but keeps it out of full clears", () => {
|
|
const dungeon = requireDungeonDefinition("deadmines");
|
|
const final = finalDungeonBoss(dungeon)!;
|
|
const run = useDungeonRunStore.getState();
|
|
run.begin(dungeon.id, dungeon.defaultDifficultyId); run.start(1000, party);
|
|
run.advance([], [final.id], 2000, party);
|
|
expect(useDungeonRunStore.getState().progress?.finishedAt).toBeNull();
|
|
run.advance([final.id], [final.id], 3000, party);
|
|
expect(completedDungeonRun(useDungeonRunStore.getState().progress!)).toMatchObject({ durationMs: 2000, fullClear: false });
|
|
});
|
|
|
|
it("keeps the original wall-clock timer and boss history through a wipe and a saved resume", () => {
|
|
const dungeon = requireDungeonDefinition("deadmines");
|
|
const run = useDungeonRunStore.getState();
|
|
run.begin(dungeon.id, "normal"); run.start(1000, party);
|
|
run.advance([], [dungeon.bosses[0].id], 2000, party);
|
|
const saved = normalizeDungeonRunProgress(JSON.parse(JSON.stringify(useDungeonRunStore.getState().progress)))!;
|
|
run.begin(dungeon.id, "normal", saved); run.start(5000, party);
|
|
run.advance([finalDungeonBoss(dungeon)!.id], [], 9000, party);
|
|
expect(useDungeonRunStore.getState().progress).toMatchObject({ id: saved.id, startedAt: 1000,
|
|
durationMs: 8000, killedBossIds: expect.arrayContaining([dungeon.bosses[0].id]) });
|
|
});
|
|
|
|
it("announces legacy resumed runs without inventing a leaderboard time", () => {
|
|
const run = useDungeonRunStore.getState(); run.begin("barren-caves", "normal", undefined, true);
|
|
run.start(1000, party); run.advance([finalDungeonBoss(requireDungeonDefinition("barren-caves"))!.id], [], 2000, party);
|
|
expect(useDungeonRunStore.getState().progress?.finishedAt).toBe(2000);
|
|
expect(completedDungeonRun(useDungeonRunStore.getState().progress!)).toBeNull();
|
|
});
|
|
|
|
it("rejects corrupted saved timers and impossible party levels", () => {
|
|
useDungeonRunStore.getState().begin("barren-caves", "normal");
|
|
const progress = useDungeonRunStore.getState().progress!;
|
|
expect(normalizeDungeonRunProgress({ ...progress, startedAt: NaN })).toBeNull();
|
|
expect(normalizeDungeonRunProgress({ ...progress, startedAt: 100, finishedAt: 50, durationMs: 10 })).toBeNull();
|
|
expect(normalizeDungeonRunProgress({ ...progress, party: [{ ...party[0], level: 81 }] })).toBeNull();
|
|
expect(formatDungeonDuration(123456)).toBe("02:03.4");
|
|
});
|
|
});
|