Files
healer-man/src/game/wowClassicEncounterContentCatalog.test.ts
T
2026-09-09 11:21:33 -04:00

335 lines
19 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { gunzipSync, strFromU8 } from "fflate";
import {
createDungeonEncounterSnapshot,
reduceDungeonEncounter,
resetDungeonEncounterSnapshot,
validateDungeonEncounterDefinition,
} from "./dungeonEncounterRuntime";
import type { DungeonDefinition, DungeonEncounterSpawnGroupDefinition } from "./dungeonTypes";
import type { DungeonEncounterDefinition, DungeonEncounterSnapshot } from "./dungeonEncounterTypes";
import { validateDungeonWorldObject } from "./dungeonWorldObjects";
import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed";
import {
WOW_CLASSIC_ENCOUNTER_CONTENT_REVIEWS,
withWowClassicEncounterContent,
} from "./wowClassicEncounterContentCatalog";
function decodeBase64(value: string): Uint8Array {
const decoded = atob(value);
const bytes = new Uint8Array(decoded.length);
for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
return bytes;
}
const campaign = JSON.parse(strFromU8(gunzipSync(decodeBase64(compressedCampaign)))) as {
readonly definitions: readonly DungeonDefinition[];
};
function adapted(id: string): DungeonDefinition {
const definition = campaign.definitions.find((candidate) => candidate.id === id);
if (!definition) throw new Error(`Missing generated dungeon ${id}`);
return withWowClassicEncounterContent(definition);
}
function classicEncounter(definition: DungeonDefinition): DungeonEncounterDefinition {
const encounter = definition.encounters?.find((value) => value.id === `${definition.id}:classic-instance`);
if (!encounter) throw new Error(`Missing Classic encounter for ${definition.id}`);
return encounter;
}
function group(definition: DungeonDefinition, id: string): DungeonEncounterSpawnGroupDefinition {
const value = definition.encounterSpawnGroups?.find((candidate) => candidate.id === id);
if (!value) throw new Error(`Missing spawn group ${id}`);
return value;
}
function objectByTemplate(definition: DungeonDefinition, templateId: number) {
const value = definition.serverGameObjects.find((object) => object.source?.templateId === templateId);
if (!value) throw new Error(`Missing gameobject template ${templateId}`);
return value;
}
function killGroup(
encounter: DungeonEncounterDefinition,
snapshot: DungeonEncounterSnapshot,
spawnGroup: DungeonEncounterSpawnGroupDefinition,
now: number,
) {
const ids = spawnGroup.spawns.map((spawn) => spawn.id);
return reduceDungeonEncounter(encounter, snapshot, {
kind: "tick",
now,
facts: { registeredEntityIds: ids, deadEntityIds: ids },
});
}
function staticEntries(definition: DungeonDefinition): readonly number[] {
return definition.staticSpawns.flatMap((spawn) => {
const entry = definition.entities[spawn.entityId]?.combat?.serverEntry;
return entry === undefined ? [] : [entry];
});
}
describe("Classic source-backed dungeon encounter content", () => {
it("pins source reviews, validates references, suppresses event bosses, and is idempotent", () => {
const cases = [
["blackfathom-deeps", []],
["uldaman", [2_748]],
["razorfen-downs", [7_355, 7_356]],
["zulfarrak", [7_273]],
] as const;
expect(WOW_CLASSIC_ENCOUNTER_CONTENT_REVIEWS.map((review) => review.dungeonId)).toEqual(cases.map(([id]) => id));
for (const review of WOW_CLASSIC_ENCOUNTER_CONTENT_REVIEWS) {
expect(review.fidelity).toBe("source-backed-adapted");
expect(review.sourceFiles.length).toBeGreaterThan(0);
for (const sourceFile of review.sourceFiles) {
expect(sourceFile.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(sourceFile.anchors.length).toBeGreaterThan(0);
expect(sourceFile.anchors.every((anchor) => /lines? \d+/.test(anchor))).toBe(true);
}
expect(review.adaptations.length).toBeGreaterThan(0);
expect(review.remainingGaps.length).toBeGreaterThan(0);
}
for (const [id, suppressedEntries] of cases) {
const definition = adapted(id);
const encounter = classicEncounter(definition);
validateDungeonEncounterDefinition(encounter);
expect(withWowClassicEncounterContent(definition)).toBe(definition);
if (suppressedEntries.length > 0) {
expect(staticEntries(definition)).not.toEqual(expect.arrayContaining([...suppressedEntries]));
}
const spawnGroupIds = new Set(definition.encounterSpawnGroups?.map((spawnGroup) => spawnGroup.id));
const objectIds = new Set(definition.serverGameObjects.map((object) => object.id));
const objectiveIds = new Set(definition.bosses.map((boss) => boss.id));
for (const spawnGroup of definition.encounterSpawnGroups ?? []) {
for (const spawn of spawnGroup.spawns) {
expect(definition.entities[spawn.entityId], `${id}:${spawnGroup.id}:${spawn.id}`).toBeDefined();
}
}
for (const rule of encounter.rules) {
if (rule.trigger.kind === "interact") expect(objectIds.has(rule.trigger.objectId), `${id}:${rule.id}`).toBe(true);
for (const action of rule.actions) {
if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") {
expect(spawnGroupIds.has(action.spawnGroupId), `${id}:${rule.id}:${action.id}`).toBe(true);
}
if (action.kind === "set-world-object-state") {
expect(objectIds.has(action.objectId), `${id}:${rule.id}:${action.id}`).toBe(true);
}
if (action.kind === "complete-objective") {
expect(objectiveIds.has(action.objectiveId), `${id}:${rule.id}:${action.id}`).toBe(true);
}
}
}
for (const object of definition.serverGameObjects.filter((candidate) => (
encounter.initialState?.worldObjectStates?.[candidate.id] !== undefined
))) {
validateDungeonWorldObject(object);
expect(object.visual?.hidden).not.toBe(true);
}
for (const entry of suppressedEntries) {
const entityIds = new Set(Object.entries(definition.entities)
.filter(([, entity]) => entity.combat?.serverEntry === entry)
.map(([entityId]) => entityId));
const objective = definition.bosses.find((boss) => definition.encounterSpawnGroups?.some(
(spawnGroup) => spawnGroup.spawns.some((spawn) => spawn.id === boss.id && entityIds.has(spawn.entityId)),
));
expect(objective, `${id}:${entry}`).toMatchObject({ eventOnly: true });
}
}
});
it("runs all four Blackfathom fires, opens the physical portal, and persists its checkpoint", () => {
const definition = adapted("blackfathom-deeps");
const encounter = classicEncounter(definition);
const fires = [21_118, 21_119, 21_120, 21_121].map((entry) => objectByTemplate(definition, entry));
let snapshot = createDungeonEncounterSnapshot(encounter);
fires.forEach((fire, index) => {
snapshot = reduceDungeonEncounter(encounter, snapshot, {
kind: "interact",
objectId: fire.id,
now: index * 10,
}).snapshot;
const spawnGroup = group(definition, `blackfathom-deeps:fire-${index + 1}-pack`);
expect(snapshot.activeSpawnGroupIds).toContain(spawnGroup.id);
snapshot = killGroup(encounter, snapshot, spawnGroup, index * 10 + 1).snapshot;
expect(snapshot.worldObjectStates[fire.id]).toBe("lit");
});
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 100 }).snapshot;
const portal = objectByTemplate(definition, 21_117);
expect(snapshot).toMatchObject({ counters: { "completed-fires": 4 }, checkpointId: "blackfathom-fire-event" });
expect(snapshot.worldObjectStates[portal.id]).toBe("open");
const reset = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 200);
expect(reset.worldObjectStates[portal.id]).toBe("open");
expect(fires.every((fire) => reset.worldObjectStates[fire.id] === "lit")).toBe(true);
expect(reset.activeSpawnGroupIds).toEqual([]);
});
it("opens Uldaman's seal, awakens four keepers with wipe recovery, and source-gates Archaedas", () => {
const definition = adapted("uldaman");
const encounter = classicEncounter(definition);
const keystone = objectByTemplate(definition, 124_371);
const seal = objectByTemplate(definition, 124_372);
const keeperAltar = objectByTemplate(definition, 130_511);
const keeperDoor = objectByTemplate(definition, 124_367);
const archaedasAltar = objectByTemplate(definition, 133_234);
const archaedasRoom = objectByTemplate(definition, 141_869);
const vault = objectByTemplate(definition, 124_369);
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact",
objectId: keystone.id,
now: 0,
}).snapshot;
expect(snapshot.worldObjectStates[seal.id]).toBe("open");
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: keeperAltar.id, now: 1 }).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, "uldaman:stone-keeper-1"), 2).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: keeperAltar.id, now: 3 }).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual(["uldaman:stone-keeper-2"]);
snapshot = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 4);
expect(snapshot).toMatchObject({ phase: "active", counters: { "keepers-defeated": 1 } });
expect(snapshot.activeSpawnGroupIds).toEqual([]);
for (let number = 2; number <= 4; number += 1) {
snapshot = reduceDungeonEncounter(encounter, snapshot, {
kind: "interact",
objectId: keeperAltar.id,
now: number * 10,
}).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, `uldaman:stone-keeper-${number}`), number * 10 + 1).snapshot;
}
expect(snapshot).toMatchObject({ phase: "active", counters: { "keepers-defeated": 4 }, checkpointId: "uldaman-stone-keepers" });
expect(snapshot.worldObjectStates[keeperDoor.id]).toBe("open");
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: archaedasAltar.id, now: 50 }).snapshot;
expect(snapshot).toMatchObject({ phase: "archaedas-active", activeSpawnGroupIds: ["uldaman:archaedas"] });
expect(snapshot.worldObjectStates[archaedasRoom.id]).toBe("closed");
snapshot = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 51);
expect(snapshot).toMatchObject({ phase: "active", activeSpawnGroupIds: [] });
expect(snapshot.worldObjectStates[archaedasRoom.id]).toBe("open");
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: archaedasAltar.id, now: 52 }).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, "uldaman:archaedas"), 53).snapshot;
const objective = definition.bosses.find((boss) => boss.name === "Archaedas")!;
expect(snapshot.completedObjectiveIds).toContain(objective.id);
expect(snapshot.worldObjectStates[vault.id]).toBe("open");
expect(snapshot.worldObjectStates[archaedasRoom.id]).toBe("open");
});
it("recovers Razorfen's gong and runs the full timed Belnistrasz event", () => {
const definition = adapted("razorfen-downs");
const encounter = classicEncounter(definition);
const gong = objectByTemplate(definition, 148_917);
const gongGroupIds = [
"razorfen-downs:gong-fiends",
"razorfen-downs:gong-reavers",
"razorfen-downs:tutenkash",
];
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact",
objectId: gong.id,
now: 0,
}).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual([gongGroupIds[0]]);
snapshot = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 1);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 1 }).snapshot;
expect(snapshot).toMatchObject({ phase: "idle", counters: { "gong-round": 0 } });
for (let index = 0; index < gongGroupIds.length; index += 1) {
snapshot = reduceDungeonEncounter(encounter, snapshot, {
kind: "interact",
objectId: gong.id,
now: 10 + index * 2,
}).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, gongGroupIds[index]), 11 + index * 2).snapshot;
}
const tutenkash = definition.bosses.find((boss) => boss.name === "Tuten'kash")!;
expect(snapshot).toMatchObject({ counters: { "gong-round": 3 } });
expect(snapshot.completedObjectiveIds).toContain(tutenkash.id);
expect(snapshot.worldObjectStates[gong.id]).toBe("complete");
const belnistrasz = definition.serverGameObjects.find((object) => object.id === "razorfen-downs:belnistrasz")!;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: belnistrasz.id, now: 100 }).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 8_100 }).snapshot;
expect(snapshot.activeSpawnGroupIds).toContain("razorfen-downs:idol-wave-1");
const wiped = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 8_101);
const recovered = reduceDungeonEncounter(encounter, wiped, { kind: "tick", now: 8_101 }).snapshot;
expect(recovered).toMatchObject({ phase: "idle", activeSpawnGroupIds: [] });
expect(recovered.worldObjectStates[belnistrasz.id]).toBe("ready");
snapshot = reduceDungeonEncounter(encounter, recovered, { kind: "interact", objectId: belnistrasz.id, now: 9_000 }).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 17_000 }).snapshot;
for (let wave = 2; wave <= 8; wave += 1) {
snapshot = reduceDungeonEncounter(encounter, snapshot, {
kind: "tick",
now: snapshot.phaseStartedAt + 35_000,
}).snapshot;
expect(snapshot.activeSpawnGroupIds).toContain(`razorfen-downs:idol-wave-${wave}`);
}
const plaguemawGroup = group(definition, "razorfen-downs:idol-wave-8");
snapshot = killGroup(encounter, snapshot, plaguemawGroup, snapshot.phaseStartedAt + 1).snapshot;
const plaguemaw = definition.bosses.find((boss) => boss.name === "Plaguemaw the Rotting")!;
expect(snapshot.completedObjectiveIds).toContain(plaguemaw.id);
snapshot = reduceDungeonEncounter(encounter, snapshot, {
kind: "tick",
now: snapshot.phaseStartedAt + 55_000,
}).snapshot;
expect(snapshot).toMatchObject({ phase: "idol-complete", flags: { "idol-extinguished": true } });
const fires = definition.serverGameObjects.filter((object) => [151_951, 151_952, 151_973].includes(object.source?.templateId ?? -1));
expect(fires.every((fire) => snapshot.worldObjectStates[fire.id] === "extinguished")).toBe(true);
const resetComplete = resetDungeonEncounterSnapshot(encounter, snapshot, "checkpoint", snapshot.phaseStartedAt + 1);
const resumedComplete = reduceDungeonEncounter(encounter, resetComplete, { kind: "tick", now: snapshot.phaseStartedAt + 1 }).snapshot;
expect(resumedComplete).toMatchObject({ phase: "idol-complete", flags: { "idol-extinguished": true } });
});
it("runs Zul'Farrak's exact 23/24/7 pyramid and independently recovers the Gahz'rilla gong", () => {
const definition = adapted("zulfarrak");
const encounter = classicEncounter(definition);
const groups = [1, 2, 3].map((wave) => group(definition, `zulfarrak:pyramid-wave-${wave}`));
expect(groups.map((spawnGroup) => spawnGroup.spawns.length)).toEqual([23, 24, 7]);
const cages = definition.serverGameObjects.filter((object) => object.name === "Troll Cage");
const endDoor = objectByTemplate(definition, 146_084);
const weegli = definition.serverGameObjects.find((object) => object.id === "zulfarrak:weegli-gate-charge")!;
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact",
objectId: cages[0].id,
now: 0,
}).snapshot;
expect(cages.every((cage) => snapshot.worldObjectStates[cage.id] === "open")).toBe(true);
snapshot = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 1);
expect(snapshot).toMatchObject({ phase: "idle", activeSpawnGroupIds: [] });
expect(cages.every((cage) => snapshot.worldObjectStates[cage.id] === "closed")).toBe(true);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: cages[2].id, now: 2 }).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 5_002 }).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual([groups[0].id]);
snapshot = killGroup(encounter, snapshot, groups[0], 5_003).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 15_003 }).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual([groups[1].id]);
snapshot = killGroup(encounter, snapshot, groups[1], 15_004).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual([groups[2].id]);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 20_004 }).snapshot;
snapshot = killGroup(encounter, snapshot, groups[2], 20_005).snapshot;
expect(snapshot).toMatchObject({ phase: "pyramid-aftermath", activeSpawnGroupIds: [] });
expect(snapshot.worldObjectStates[weegli.id]).toBe("ready");
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: weegli.id, now: 20_006 }).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 24_006 }).snapshot;
expect(snapshot).toMatchObject({ phase: "pyramid-complete", flags: { "pyramid-complete": true }, checkpointId: "zulfarrak-pyramid" });
expect(snapshot.worldObjectStates[endDoor.id]).toBe("open");
const reset = resetDungeonEncounterSnapshot(encounter, snapshot, "checkpoint", 24_007);
expect(reset.worldObjectStates[endDoor.id]).toBe("open");
expect(cages.every((cage) => reset.worldObjectStates[cage.id] === "open")).toBe(true);
const gong = objectByTemplate(definition, 141_832);
snapshot = reduceDungeonEncounter(encounter, reset, { kind: "interact", objectId: gong.id, now: 25_000 }).snapshot;
const gahzrillaGroup = group(definition, "zulfarrak:gahzrilla");
expect(snapshot.activeSpawnGroupIds).toEqual([gahzrillaGroup.id]);
const gongWipe = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", 25_001);
expect(gongWipe.worldObjectStates[gong.id]).toBe("ready");
snapshot = reduceDungeonEncounter(encounter, gongWipe, { kind: "interact", objectId: gong.id, now: 25_002 }).snapshot;
snapshot = killGroup(encounter, snapshot, gahzrillaGroup, 25_003).snapshot;
const gahzrilla = definition.bosses.find((boss) => boss.name === "Ghaz'rilla")!;
expect(snapshot.completedObjectiveIds).toContain(gahzrilla.id);
expect(snapshot.worldObjectStates[gong.id]).toBe("complete");
});
});