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

353 lines
19 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { gunzipSync, strFromU8 } from "fflate";
import {
createDungeonEncounterSnapshot,
reduceDungeonEncounter,
resetDungeonEncounterSnapshot,
validateDungeonEncounterDefinition,
} from "./dungeonEncounterRuntime";
import { activeEncounterSpawnGroupSpawns } from "./dungeonEncounterSpawns";
import type { DungeonDefinition, DungeonEncounterSpawnGroupDefinition } from "./dungeonTypes";
import type { DungeonEncounterDefinition, DungeonEncounterSnapshot } from "./dungeonEncounterTypes";
import { validateDungeonWorldObject } from "./dungeonWorldObjects";
import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed";
import {
WOW_ENCOUNTER_CONTENT_REVIEWS,
WOW_ENCOUNTER_NEXT_BATCH,
withWowEncounterContent,
} from "./wowEncounterContentCatalog";
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 withWowEncounterContent(definition);
}
function wowEncounter(definition: DungeonDefinition): DungeonEncounterDefinition {
const encounter = definition.encounters?.find((value) => value.id === `${definition.id}:wow-instance`);
if (!encounter) throw new Error(`Missing WoW 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 staticEntries(definition: DungeonDefinition): readonly number[] {
return definition.staticSpawns.flatMap((spawn) => {
const entry = definition.entities[spawn.entityId]?.combat?.serverEntry;
return entry === undefined ? [] : [entry];
});
}
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 },
});
}
describe("source-backed WoW encounter content", () => {
it("removes every adapted event boss from static population and validates its encounter", () => {
const cases = [
["black-morass", [17_879, 17_880, 17_881]],
["violet-hold", [29_315, 29_316, 31_134]],
["culling-of-stratholme", [26_529, 26_530, 26_532, 26_533]],
["escape-from-durnholde", [17_848, 17_862, 18_096]],
["arcatraz", [20_912]],
["blackrock-spire", [10_264]],
] as const;
for (const [id, entries] of cases) {
const definition = adapted(id);
const encounter = wowEncounter(definition);
validateDungeonEncounterDefinition(encounter);
expect(encounter.source?.fidelity).toBe("source-backed-adapted");
expect(encounter.source?.scope).toBe("instance-orchestration");
const remainingStaticEntries = staticEntries(definition);
for (const entry of entries) {
expect(remainingStaticEntries, `${id} static entry ${entry}`).not.toContain(entry);
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 ${entry}`).toMatchObject({ eventOnly: true });
}
expect(withWowEncounterContent(definition)).toBe(definition);
}
expect(staticEntries(adapted("violet-hold"))).not.toContain(29_395);
expect(WOW_ENCOUNTER_CONTENT_REVIEWS.map((review) => review.dungeonId)).toEqual(expect.arrayContaining(cases.map(([id]) => id)));
expect(WOW_ENCOUNTER_NEXT_BATCH).toEqual([]);
});
it("resumes Black Morass at rift 7 and 13 after run-persistent boss checkpoints", () => {
const definition = adapted("black-morass");
const encounter = wowEncounter(definition);
let snapshot = reduceDungeonEncounter(
encounter,
createDungeonEncounterSnapshot(encounter),
{ kind: "interact", objectId: "black-morass:medivh-defense-start", now: 0 },
).snapshot;
let now = 0;
for (let rift = 1; rift <= 6; rift += 1) {
now = snapshot.phaseStartedAt + (rift === 1 ? 9_000 : 7_000);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
const spawnGroup = group(definition, `black-morass:rift-${String(rift).padStart(2, "0")}`);
expect(snapshot.activeSpawnGroupIds).toContain(spawnGroup.id);
snapshot = killGroup(encounter, snapshot, spawnGroup, now + 1).snapshot;
}
const deja = definition.bosses.find((boss) => boss.name === "Chrono Lord Deja")!;
expect(snapshot).toMatchObject({ phase: "rift-07-wait", checkpointId: "after-deja" });
expect(snapshot.completedObjectiveIds).toContain(deja.id);
now = snapshot.phaseStartedAt + 156_000;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual(["black-morass:rift-07"]);
const resetAfterDeja = resetDungeonEncounterSnapshot(encounter, snapshot, "boss-wipe", now + 1);
expect(resetAfterDeja.activeSpawnGroupIds).toEqual([]);
expect(resetAfterDeja.completedObjectiveIds).toEqual([deja.id]);
expect(activeEncounterSpawnGroupSpawns(definition.encounterSpawnGroups ?? [], resetAfterDeja.activeSpawnGroupIds, 1)).toEqual([]);
snapshot = reduceDungeonEncounter(encounter, resetAfterDeja, { kind: "tick", now: now + 1 }).snapshot;
expect(snapshot).toMatchObject({ phase: "rift-07-wait", checkpointId: "after-deja", counters: { rift: 6 } });
for (let rift = 7; rift <= 12; rift += 1) {
now = snapshot.phaseStartedAt + (rift === 7 ? 156_000 : 7_000);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
const spawnGroup = group(definition, `black-morass:rift-${String(rift).padStart(2, "0")}`);
snapshot = killGroup(encounter, snapshot, spawnGroup, now + 1).snapshot;
}
const temporus = definition.bosses.find((boss) => boss.name === "Temporus")!;
expect(snapshot).toMatchObject({ phase: "rift-13-wait", checkpointId: "after-temporus" });
expect(snapshot.completedObjectiveIds).toEqual(expect.arrayContaining([deja.id, temporus.id]));
now = snapshot.phaseStartedAt + 156_000;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual(["black-morass:rift-13"]);
const resetAfterTemporus = resetDungeonEncounterSnapshot(encounter, snapshot, "checkpoint", now + 1);
expect(resetAfterTemporus.activeSpawnGroupIds).toEqual([]);
snapshot = reduceDungeonEncounter(encounter, resetAfterTemporus, { kind: "tick", now: now + 1 }).snapshot;
expect(snapshot).toMatchObject({ phase: "rift-13-wait", checkpointId: "after-temporus", counters: { rift: 12 } });
});
it("makes reviewed Violet Hold doors visible and physically stateful", () => {
const definition = adapted("violet-hold");
const reviewedTemplates = new Set([191_723, 191_564, 191_563, 191_562, 191_606]);
const doors = definition.serverGameObjects.filter((object) => reviewedTemplates.has(object.source?.templateId ?? -1));
expect(doors.length).toBeGreaterThan(0);
for (const door of doors) {
validateDungeonWorldObject(door);
expect(door.states.find((state) => state.id === "closed")).toMatchObject({
visible: true,
blocksMovement: true,
blocksSight: true,
});
expect(door.states.find((state) => state.id === "open")).toMatchObject({
visible: true,
blocksMovement: false,
blocksSight: false,
});
expect(door.visual?.hidden).not.toBe(true);
}
});
it("runs all 18 Violet Hold waves and persists its three objectives", () => {
const definition = adapted("violet-hold");
const encounter = wowEncounter(definition);
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact", objectId: "violet-hold:sinclari-start", now: 0,
}).snapshot;
let now = 0;
for (let wave = 1; wave <= 18; wave += 1) {
const id = `violet-hold:wave-${String(wave).padStart(2, "0")}`;
if (!snapshot.activeSpawnGroupIds.includes(id)) {
const delay = wave === 1 ? 19_000 : wave === 7 || wave === 13 ? 35_000 : 3_000;
now = snapshot.phaseStartedAt + delay;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
expect(snapshot.activeSpawnGroupIds, id).toContain(id);
snapshot = killGroup(encounter, snapshot, group(definition, id), now + 1).snapshot;
now += 1;
}
expect(snapshot).toMatchObject({ phase: "complete", flags: { "dungeon-complete": true }, activeSpawnGroupIds: [] });
expect(snapshot.completedObjectiveIds).toEqual(expect.arrayContaining(definition.bosses.map((boss) => boss.id)));
});
it("persists Black Morass completion and deactivates the final group", () => {
const definition = adapted("black-morass");
const encounter = wowEncounter(definition);
const aeonus = definition.bosses.find((boss) => boss.name === "Aeonus")!;
const finalGroup = group(definition, "black-morass:rift-18");
const base = createDungeonEncounterSnapshot(encounter, 1_000);
const readyToClear = {
...base,
phase: "rift-18-active",
activeSpawnGroupIds: [finalGroup.id],
} as DungeonEncounterSnapshot;
const cleared = killGroup(encounter, readyToClear, finalGroup, 2_000);
expect(cleared.snapshot).toMatchObject({
phase: "complete",
flags: { "dungeon-complete": true },
activeSpawnGroupIds: [],
});
expect(cleared.snapshot.completedObjectiveIds).toContain(aeonus.id);
expect(cleared.effects).toContainEqual({
kind: "despawn-group",
actionId: "deactivate-rift-18",
spawnGroupId: finalGroup.id,
});
const reset = resetDungeonEncounterSnapshot(encounter, cleared.snapshot, "boss-wipe", 3_000);
const recovered = reduceDungeonEncounter(encounter, reset, { kind: "tick", now: 3_000 }).snapshot;
expect(recovered).toMatchObject({ phase: "complete", flags: { "dungeon-complete": true } });
expect(recovered.completedObjectiveIds).toContain(aeonus.id);
});
it("runs Culling's city, rift, Epoch, and Mal'Ganis groups to completion", () => {
const definition = adapted("culling-of-stratholme");
const encounter = wowEncounter(definition);
const sequence = [
...Array.from({ length: 10 }, (_, index) => `culling-city-${String(index + 1).padStart(2, "0")}`),
"culling-town-01", "culling-town-02", "culling-town-03", "culling-epoch", "culling-malganis",
];
const delays = new Map<string, number>([[sequence[0], 10_000], ["culling-town-01", 5_000], ["culling-epoch", 32_000], ["culling-malganis", 5_000]]);
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact", objectId: "culling-of-stratholme:arthas-start", now: 0,
}).snapshot;
let now = 0;
for (const id of sequence) {
if (!snapshot.activeSpawnGroupIds.includes(id)) {
now = snapshot.phaseStartedAt + (delays.get(id) ?? 0);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
expect(snapshot.activeSpawnGroupIds, id).toContain(id);
snapshot = killGroup(encounter, snapshot, group(definition, id), now + 1).snapshot;
now += 1;
}
expect(snapshot).toMatchObject({ phase: "complete", flags: { "dungeon-complete": true }, activeSpawnGroupIds: [] });
expect(snapshot.completedObjectiveIds).toEqual(expect.arrayContaining(definition.bosses.map((boss) => boss.id)));
});
it("uses five imported Durnholde barrels before Drake, then gates the Thrall sequence", () => {
const definition = adapted("escape-from-durnholde");
const encounter = wowEncounter(definition);
const barrels = definition.serverGameObjects.filter((object) => object.source?.templateId === 182_589);
expect(barrels.length).toBeGreaterThanOrEqual(5);
expect(barrels.every((object) => object.interaction?.prompt === "Plant an incendiary bomb")).toBe(true);
let snapshot = createDungeonEncounterSnapshot(encounter);
barrels.slice(0, 5).forEach((barrel, index) => {
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: barrel.id, now: index }).snapshot;
});
expect(snapshot.counters.barrels).toBe(5);
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 5 }).snapshot;
expect(snapshot.phase).toBe("durnholde-drake-wait");
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 12_005 }).snapshot;
const drakeGroup = group(definition, "durnholde-drake");
snapshot = killGroup(encounter, snapshot, drakeGroup, 12_006).snapshot;
const drake = definition.bosses.find((boss) => boss.name === "Lieutenant Drake")!;
expect(snapshot).toMatchObject({ phase: "durnholde-await-thrall", checkpointId: drake.id });
const prisonDoor = definition.serverGameObjects.find((object) => object.source?.templateId === 184_393)!;
expect(snapshot.worldObjectStates[prisonDoor.id]).toBe("open");
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: "escape-from-durnholde:thrall-escort", now: 12_007 }).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 12_007 }).snapshot;
expect(snapshot.activeSpawnGroupIds).toContain("durnholde-skarloc");
const reset = resetDungeonEncounterSnapshot(encounter, killGroup(encounter, snapshot, group(definition, "durnholde-skarloc"), 12_008).snapshot, "boss-wipe", 12_009);
const recovered = reduceDungeonEncounter(encounter, reset, { kind: "tick", now: 12_009 }).snapshot;
expect(recovered.phase).toBe("durnholde-barn-wait");
expect(recovered.activeSpawnGroupIds).toEqual([]);
snapshot = recovered;
let now = 12_009;
const remainder = [
["durnholde-barn", 3_000],
["durnholde-church", 3_000],
["durnholde-epoch-wave-1", 3_000],
["durnholde-epoch-wave-2", 0],
["durnholde-epoch-wave-3", 0],
["durnholde-epoch", 8_000],
] as const;
for (const [id, delay] of remainder) {
if (!snapshot.activeSpawnGroupIds.includes(id)) {
now = snapshot.phaseStartedAt + delay;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
expect(snapshot.activeSpawnGroupIds, id).toContain(id);
snapshot = killGroup(encounter, snapshot, group(definition, id), now + 1).snapshot;
now += 1;
}
expect(snapshot).toMatchObject({ phase: "complete", flags: { "dungeon-complete": true }, activeSpawnGroupIds: [] });
});
it("opens reviewed Arcatraz pods in order before spawning Skyriss", () => {
const definition = adapted("arcatraz");
const encounter = wowEncounter(definition);
const pods = [183_961, 183_963, 183_964, 183_962, 183_965].map((templateId) => (
definition.serverGameObjects.find((object) => object.source?.templateId === templateId)!
));
for (const pod of pods) {
validateDungeonWorldObject(pod);
expect(pod.states.find((state) => state.id === "closed")).toMatchObject({ blocksMovement: true, blocksSight: true, visible: true });
expect(pod.states.find((state) => state.id === "open")).toMatchObject({ blocksMovement: false, blocksSight: false, visible: true });
expect(pod.visual?.hidden).not.toBe(true);
}
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact", objectId: "arcatraz:warden-start", now: 0,
}).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 5_000 }).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, "arcatraz-pod-alpha"), 5_001).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 10_001 }).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 20_001 }).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, "arcatraz-pod-delta"), 20_002).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 28_002 }).snapshot;
snapshot = killGroup(encounter, snapshot, group(definition, "arcatraz-pod-gamma"), 28_003).snapshot;
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 40_003 }).snapshot;
expect(snapshot.activeSpawnGroupIds).toEqual(["arcatraz-skyriss"]);
expect(pods.every((pod) => snapshot.worldObjectStates[pod.id] === "open")).toBe(true);
snapshot = killGroup(encounter, snapshot, group(definition, "arcatraz-skyriss"), 40_004).snapshot;
expect(snapshot).toMatchObject({ phase: "complete", activeSpawnGroupIds: [] });
});
it("keeps Solakar waves overlapping and removes surviving adds when Solakar dies", () => {
const definition = adapted("blackrock-spire");
const encounter = wowEncounter(definition);
const fatherFlame = definition.serverGameObjects.find((object) => object.source?.templateId === 175_245)!;
expect(fatherFlame.interaction?.prompt).toBe("Activate Father Flame");
let snapshot = reduceDungeonEncounter(encounter, createDungeonEncounterSnapshot(encounter), {
kind: "interact", objectId: fatherFlame.id, now: 0,
}).snapshot;
for (const now of [500, 30_500, 60_500, 90_500, 120_500, 150_500]) {
snapshot = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
expect(snapshot.activeSpawnGroupIds).toHaveLength(6);
expect(snapshot.activeSpawnGroupIds).toContain("blackrock-spire:solakar");
const solakarGroup = group(definition, "blackrock-spire:solakar");
const cleared = killGroup(encounter, snapshot, solakarGroup, 150_501);
expect(cleared.snapshot).toMatchObject({ phase: "complete", activeSpawnGroupIds: [] });
expect(cleared.effects.filter((effect) => effect.kind === "despawn-group")).toHaveLength(6);
expect(activeEncounterSpawnGroupSpawns(definition.encounterSpawnGroups ?? [], cleared.snapshot.activeSpawnGroupIds, 1)).toEqual([]);
});
});