Files
healer-man/src/game/wowWrathEncounterBatchTwoCatalog.test.ts
T
2026-09-16 14:30:58 -04:00

700 lines
30 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { gunzipSync, strFromU8 } from "fflate";
import { describe, expect, it } from "vitest";
import {
createDungeonEncounterSnapshot,
reduceDungeonEncounter,
resetDungeonEncounterSnapshot,
validateDungeonEncounterDefinition,
} from "./dungeonEncounterRuntime";
import type {
DungeonEncounterAction,
DungeonEncounterDefinition,
DungeonEncounterGuard,
DungeonEncounterSnapshot,
DungeonEncounterTrigger,
} from "./dungeonEncounterTypes";
import type { DungeonBossObjective, DungeonDefinition } from "./dungeonTypes";
import { validateDungeonWorldObject } from "./dungeonWorldObjects";
import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed";
import {
WOW_WRATH_ENCOUNTER_BATCH_TWO_REVIEWS,
withWowWrathEncounterBatchTwoContent,
} from "./wowWrathEncounterBatchTwoCatalog";
const TARGETS = [
"halls-of-stone",
"pit-of-saron",
"utgarde-pinnacle",
"gundrak",
] as const;
type TargetId = typeof TARGETS[number];
const REVIEWED_TEMPLATES: Readonly<Record<TargetId, readonly number[]>> = Object.freeze({
"halls-of-stone": [193_907, 193_906, 191_296],
"pit-of-saron": [201_885, 201_848],
"utgarde-pinnacle": [191_745, 188_593, 192_173, 192_174],
gundrak: [
192_518, 192_519, 192_520,
192_564, 192_565, 192_567, 192_566,
193_188, 192_633, 192_632, 192_569,
192_568, 193_208, 193_209,
],
});
const EVENT_BOSS_NAMES: Readonly<Record<TargetId, readonly string[]>> = Object.freeze({
"halls-of-stone": ["Tribunal of Ages"],
"pit-of-saron": ["Tyrannus"],
"utgarde-pinnacle": ["Svala", "Gortok", "Skadi"],
gundrak: ["Eck"],
});
const FINAL_BOSS_NAME: Readonly<Record<TargetId, string>> = Object.freeze({
"halls-of-stone": "Sjonnir",
"pit-of-saron": "Tyrannus",
"utgarde-pinnacle": "Ymiron",
gundrak: "Gal'darah",
});
const EXPECTED_NONFINAL_CHECKPOINTS: Readonly<Record<TargetId, number>> = Object.freeze({
"halls-of-stone": 3,
"pit-of-saron": 3,
"utgarde-pinnacle": 3,
gundrak: 4,
});
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 generatedDefinitions = (
JSON.parse(strFromU8(gunzipSync(decodeBase64(compressedCampaign)))) as {
readonly definitions: readonly DungeonDefinition[];
}
).definitions;
function sourceDefinition(id: TargetId): DungeonDefinition {
const source = generatedDefinitions.find((definition) => definition.id === id);
if (!source) throw new Error(`Missing generated dungeon ${id}`);
return source;
}
function adaptedDefinition(id: TargetId): DungeonDefinition {
return withWowWrathEncounterBatchTwoContent(sourceDefinition(id));
}
function adaptedEncounter(definition: DungeonDefinition): DungeonEncounterDefinition {
const encounter = definition.encounters?.find((candidate) => (
candidate.id === `${definition.id}:wrath-batch-two-instance`
));
if (!encounter) throw new Error(`Missing batch-two encounter for ${definition.id}`);
return encounter;
}
function normalized(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function objective(definition: DungeonDefinition, nameFragment: string): DungeonBossObjective {
const fragment = normalized(nameFragment);
const value = definition.bosses.find((candidate) => normalized(candidate.name).includes(fragment));
if (!value) throw new Error(`${definition.id} is missing objective matching ${nameFragment}`);
return value;
}
function group(definition: DungeonDefinition, id: string) {
const value = definition.encounterSpawnGroups?.find((candidate) => candidate.id === id);
if (!value) throw new Error(`${definition.id} is missing encounter group ${id}`);
return value;
}
function runtimeSpawnIds(definition: DungeonDefinition): readonly string[] {
return [
...definition.staticSpawns.map((spawn) => spawn.id),
...definition.roamingPacks.flatMap((pack) => (
pack.members.map((member) => `${pack.id}:${member.id}`)
)),
...(definition.encounterSpawnGroups ?? []).flatMap((spawnGroup) => (
spawnGroup.spawns.map((spawn) => spawn.id)
)),
];
}
function activeSpawnIds(
definition: DungeonDefinition,
snapshot: DungeonEncounterSnapshot,
ignoredGroupIds: ReadonlySet<string> = new Set(),
): readonly string[] {
const active = new Set(snapshot.activeSpawnGroupIds);
return (definition.encounterSpawnGroups ?? [])
.filter((spawnGroup) => active.has(spawnGroup.id) && !ignoredGroupIds.has(spawnGroup.id))
.flatMap((spawnGroup) => spawnGroup.spawns.map((spawn) => spawn.id));
}
function killIds(
encounter: DungeonEncounterDefinition,
snapshot: DungeonEncounterSnapshot,
ids: readonly string[],
now: number,
): DungeonEncounterSnapshot {
return reduceDungeonEncounter(encounter, snapshot, {
kind: "tick",
now,
facts: { registeredEntityIds: ids, deadEntityIds: ids },
}).snapshot;
}
function interact(
encounter: DungeonEncounterDefinition,
snapshot: DungeonEncounterSnapshot,
objectId: string,
now: number,
): DungeonEncounterSnapshot {
return reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId, now }).snapshot;
}
function incompleteObjective(
definition: DungeonDefinition,
snapshot: DungeonEncounterSnapshot,
names: readonly string[],
): DungeonBossObjective | undefined {
return names
.map((name) => objective(definition, name))
.find((candidate) => !snapshot.completedObjectiveIds.includes(candidate.id));
}
function advanceOneStep(
definition: DungeonDefinition,
encounter: DungeonEncounterDefinition,
snapshot: DungeonEncounterSnapshot,
now: number,
): DungeonEncounterSnapshot {
if (snapshot.phase === "resume") {
return reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
if (definition.id === "halls-of-stone") {
if (snapshot.phase === "tribunal-ready") {
const remaining = incompleteObjective(definition, snapshot, ["Krystallus", "Maiden"]);
return remaining
? killIds(encounter, snapshot, [remaining.id], now)
: interact(encounter, snapshot, "halls-of-stone:reviewed-tribunal-console", now);
}
if (snapshot.phase === "sjonnir-ready") {
return killIds(encounter, snapshot, [objective(definition, "Sjonnir").id], now);
}
}
if (definition.id === "pit-of-saron") {
if (snapshot.phase === "progression") {
const remaining = incompleteObjective(definition, snapshot, ["Garfrost", "Ick"]);
return remaining
? killIds(encounter, snapshot, [remaining.id], now)
: reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
if (snapshot.phase === "gauntlet-ready") {
return interact(encounter, snapshot, "pit-of-saron:gauntlet-start", now);
}
if (snapshot.phase === "tunnel-ready") {
return interact(encounter, snapshot, "pit-of-saron:tunnel-start", now);
}
}
if (definition.id === "utgarde-pinnacle") {
if (snapshot.phase === "svala-ready") {
return interact(encounter, snapshot, "utgarde-pinnacle:svala-start", now);
}
if (snapshot.phase === "svala-phase-one-active") {
return reduceDungeonEncounter(encounter, snapshot, {
kind: "entity-health",
entityId: "utgarde-pinnacle:svala-phase-one-actor",
healthPercent: 50,
now,
}).snapshot;
}
if (snapshot.phase === "gortok-ready") {
return interact(encounter, snapshot, "utgarde-pinnacle:reviewed-stasis-sphere", now);
}
if (snapshot.phase === "skadi-ready") {
return interact(encounter, snapshot, "utgarde-pinnacle:skadi-start", now);
}
const harpoonByPhase: Readonly<Record<string, string>> = {
"harpoon-one-ready": "utgarde-pinnacle:harpoon-1",
"harpoon-two-ready": "utgarde-pinnacle:harpoon-2",
"harpoon-three-ready": "utgarde-pinnacle:harpoon-3",
};
const harpoon = harpoonByPhase[snapshot.phase];
if (harpoon) return interact(encounter, snapshot, harpoon, now);
if (snapshot.phase === "ymiron-ready") {
return killIds(encounter, snapshot, [objective(definition, "Ymiron").id], now);
}
}
if (definition.id === "gundrak") {
if (snapshot.phase === "altar-progression") {
const remaining = incompleteObjective(
definition,
snapshot,
["Slad'ran", "Moorabi", "Drakkari Colossus"],
);
if (remaining) return killIds(encounter, snapshot, [remaining.id], now);
const altar = definition.serverGameObjects.find((object) => (
object.id.startsWith("gundrak:reviewed-altar-")
&& snapshot.worldObjectStates[object.id] === "ready"
));
return altar
? interact(encounter, snapshot, altar.id, now)
: reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
if (snapshot.phase === "bridge-activating") {
return reduceDungeonEncounter(encounter, snapshot, {
kind: "tick",
now: Math.max(now, snapshot.phaseStartedAt + 5_000),
}).snapshot;
}
if (snapshot.phase === "gal-darah-ready") {
return killIds(encounter, snapshot, [objective(definition, "Gal'darah").id], now);
}
return reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
const spawnIds = activeSpawnIds(definition, snapshot);
if (spawnIds.length) return killIds(encounter, snapshot, spawnIds, now);
return reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now }).snapshot;
}
interface FinishedEncounter {
readonly snapshot: DungeonEncounterSnapshot;
readonly phases: readonly string[];
readonly checkpoints: readonly DungeonEncounterSnapshot[];
}
function finishEncounter(
definition: DungeonDefinition,
encounter: DungeonEncounterDefinition,
initial = createDungeonEncounterSnapshot(encounter),
): FinishedEncounter {
let snapshot = initial;
const phases = [snapshot.phase];
const checkpoints: DungeonEncounterSnapshot[] = [];
const seenCheckpoints = new Set<string>();
const finalObjectiveId = objective(definition, FINAL_BOSS_NAME[definition.id as TargetId]).id;
let now = snapshot.phaseStartedAt + 1;
for (let step = 0; step < 100 && snapshot.phase !== "complete"; step += 1) {
const previous = snapshot;
snapshot = advanceOneStep(definition, encounter, snapshot, now);
now = Math.max(now + 1, snapshot.phaseStartedAt + 1);
if (snapshot.phase !== phases.at(-1)) phases.push(snapshot.phase);
if (snapshot.checkpointId
&& snapshot.checkpointId !== finalObjectiveId
&& !seenCheckpoints.has(snapshot.checkpointId)) {
seenCheckpoints.add(snapshot.checkpointId);
checkpoints.push(snapshot);
}
if (snapshot === previous) throw new Error(`${definition.id} soft locked in ${snapshot.phase}`);
}
if (snapshot.phase !== "complete") throw new Error(`${definition.id} did not complete`);
return { snapshot, phases, checkpoints };
}
function expectOrderedPhases(phases: readonly string[], expected: readonly string[]): void {
let cursor = -1;
for (const phase of expected) {
cursor = phases.indexOf(phase, cursor + 1);
expect(cursor, `missing ordered phase ${phase}`).toBeGreaterThanOrEqual(0);
}
}
function expectTriggerReferences(
trigger: DungeonEncounterTrigger,
spawnIds: ReadonlySet<string>,
objectIds: ReadonlySet<string>,
): void {
if (trigger.kind === "entity-death" || trigger.kind === "entity-engaged" || trigger.kind === "entity-health") {
expect(spawnIds, `trigger entity ${trigger.entityId}`).toContain(trigger.entityId);
}
if (trigger.kind === "entities-dead") {
for (const entityId of trigger.entityIds) expect(spawnIds, `trigger entity ${entityId}`).toContain(entityId);
}
if (trigger.kind === "interact") expect(objectIds, `trigger object ${trigger.objectId}`).toContain(trigger.objectId);
}
function expectGuardReferences(
guard: DungeonEncounterGuard,
groupIds: ReadonlySet<string>,
objectIds: ReadonlySet<string>,
objectiveIds: ReadonlySet<string>,
): void {
if (guard.kind === "spawn-group-active") expect(groupIds).toContain(guard.spawnGroupId);
if (guard.kind === "world-object-state") expect(objectIds).toContain(guard.objectId);
if (guard.kind === "objective-complete") expect(objectiveIds).toContain(guard.objectiveId);
}
function expectActionReferences(
action: DungeonEncounterAction,
groupIds: ReadonlySet<string>,
objectsById: ReadonlyMap<string, DungeonDefinition["serverGameObjects"][number]>,
objectiveIds: ReadonlySet<string>,
): void {
if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") {
expect(groupIds, `action group ${action.spawnGroupId}`).toContain(action.spawnGroupId);
}
if (action.kind === "set-world-object-state") {
const object = objectsById.get(action.objectId);
expect(object, `action object ${action.objectId}`).toBeDefined();
expect(object?.states.map((state) => state.id), `${action.objectId}:${action.state}`)
.toContain(action.state);
}
if (action.kind === "complete-objective") expect(objectiveIds).toContain(action.objectiveId);
}
describe("source-reviewed Wrath encounter batch two", () => {
it("pins every reviewed AzerothCore source file by actual SHA-256 and line/symbol anchors", async () => {
expect(WOW_WRATH_ENCOUNTER_BATCH_TWO_REVIEWS.map((review) => review.dungeonId)).toEqual(TARGETS);
for (const review of WOW_WRATH_ENCOUNTER_BATCH_TWO_REVIEWS) {
expect(review.fidelity).toBe("source-backed-adapted");
expect(review.adaptations.length).toBeGreaterThan(0);
expect(review.remainingGaps.length).toBeGreaterThan(0);
for (const source of review.sourceFiles) {
expect(source.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(source.anchors.length).toBeGreaterThan(0);
expect(source.anchors.every((anchor) => /lines? \d+/i.test(anchor))).toBe(true);
const digest = createHash("sha256").update(await readFile(source.path)).digest("hex");
expect(digest, source.path).toBe(source.sha256);
}
}
});
it("attaches idempotently with unique rows, valid references, and finite event groups", () => {
for (const id of TARGETS) {
const definition = adaptedDefinition(id);
const encounter = adaptedEncounter(definition);
expect(withWowWrathEncounterBatchTwoContent(definition)).toBe(definition);
expect(encounter.source).toMatchObject({
fidelity: "source-backed-adapted",
scope: "instance-orchestration",
});
validateDungeonEncounterDefinition(encounter);
const spawnIdList = runtimeSpawnIds(definition);
expect(new Set(spawnIdList).size, `${id} duplicate runtime spawn id`).toBe(spawnIdList.length);
const groupIdList = (definition.encounterSpawnGroups ?? []).map((spawnGroup) => spawnGroup.id);
expect(new Set(groupIdList).size, `${id} duplicate group id`).toBe(groupIdList.length);
const objectIdList = definition.serverGameObjects.map((object) => object.id);
expect(new Set(objectIdList).size, `${id} duplicate object id`).toBe(objectIdList.length);
const encounterIdList = (definition.encounters ?? []).map((candidate) => candidate.id);
expect(new Set(encounterIdList).size, `${id} duplicate encounter id`).toBe(encounterIdList.length);
const sourceSpawnIds = definition.serverGameObjects
.map((object) => object.source?.spawnId)
.filter((spawnId): spawnId is number => spawnId !== undefined);
expect(new Set(sourceSpawnIds).size, `${id} duplicate source gameobject row`).toBe(sourceSpawnIds.length);
const spawnIds = new Set(spawnIdList);
const groupIds = new Set(groupIdList);
const objectIds = new Set(objectIdList);
const objectsById = new Map(definition.serverGameObjects.map((object) => [object.id, object] as const));
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], `${spawnGroup.id}:${spawn.id}`).toBeDefined();
}
}
for (const object of definition.serverGameObjects) validateDungeonWorldObject(object);
for (const rule of encounter.rules) {
expectTriggerReferences(rule.trigger, spawnIds, objectIds);
for (const guard of rule.guards ?? []) {
expectGuardReferences(guard, groupIds, objectIds, objectiveIds);
}
for (const action of rule.actions) {
expectActionReferences(action, groupIds, objectsById, objectiveIds);
}
}
const batchGroups = (definition.encounterSpawnGroups ?? [])
.filter((spawnGroup) => spawnGroup.id.startsWith(`${id}:`));
for (const spawnGroup of batchGroups) {
const completion = encounter.rules.find((rule) => (
rule.trigger.kind === "entities-dead"
&& rule.trigger.entityIds.length === spawnGroup.spawns.length
&& rule.trigger.entityIds.every((spawnId) => spawnGroup.spawns.some((spawn) => spawn.id === spawnId))
));
const isHealthGatedSvala = spawnGroup.id === "utgarde-pinnacle:svala-phase-one";
expect(Boolean(completion) || isHealthGatedSvala, `${id} finite completion for ${spawnGroup.id}`).toBe(true);
if (completion?.trigger.kind === "entities-dead") {
expect(completion.trigger.timeoutMs, `${id} timeout for ${spawnGroup.id}`).toBeGreaterThan(0);
}
}
}
});
it("replaces each reviewed imported object by spawnId without dropping or duplicating source rows", () => {
for (const id of TARGETS) {
const source = sourceDefinition(id);
const definition = adaptedDefinition(id);
const sourceRowIds = source.serverGameObjects
.map((object) => object.source?.spawnId)
.filter((spawnId): spawnId is number => spawnId !== undefined)
.sort((left, right) => left - right);
const adaptedRowIds = definition.serverGameObjects
.map((object) => object.source?.spawnId)
.filter((spawnId): spawnId is number => spawnId !== undefined)
.sort((left, right) => left - right);
expect(adaptedRowIds, `${id} source-row inventory`).toEqual(sourceRowIds);
for (const templateId of REVIEWED_TEMPLATES[id]) {
const originalRows = source.serverGameObjects.filter((object) => object.source?.templateId === templateId);
expect(originalRows, `${id} template ${templateId}`).toHaveLength(1);
const original = originalRows[0];
expect(original.source?.spawnId).toEqual(expect.any(Number));
const replacements = definition.serverGameObjects.filter((object) => (
object.source?.spawnId === original.source?.spawnId
));
expect(replacements, `${id} spawn ${original.source?.spawnId}`).toHaveLength(1);
const replacement = replacements[0];
expect(replacement.id).not.toBe(original.id);
expect(replacement.id).toContain(":reviewed-");
expect(replacement.position).toEqual(original.position);
expect(replacement.halfExtents).toEqual(original.halfExtents);
expect(replacement.yaw).toEqual(original.yaw);
expect(replacement.visual?.hidden).not.toBe(true);
expect(replacement.source?.fidelity).toBe("source-backed-adapted");
}
}
expect(adaptedDefinition("halls-of-stone").serverGameObjects
.find((object) => object.id === "halls-of-stone:reviewed-tribunal-console")?.interaction)
.toBeDefined();
expect(adaptedDefinition("utgarde-pinnacle").serverGameObjects
.find((object) => object.id === "utgarde-pinnacle:reviewed-stasis-sphere")?.interaction)
.toBeDefined();
expect(adaptedDefinition("gundrak").serverGameObjects
.filter((object) => object.id.startsWith("gundrak:reviewed-altar-")).every((object) => object.interaction))
.toBe(true);
});
it("suppresses only source-gated event objectives and preserves exact finite cardinalities", () => {
for (const id of TARGETS) {
const definition = adaptedDefinition(id);
const staticIds = new Set(definition.staticSpawns.map((spawn) => spawn.id));
for (const name of EVENT_BOSS_NAMES[id]) {
const eventBoss = objective(definition, name);
expect(eventBoss.eventOnly, `${id}:${eventBoss.name}`).toBe(true);
expect(staticIds, `${id}:${eventBoss.name}`).not.toContain(eventBoss.id);
}
}
const halls = adaptedDefinition("halls-of-stone");
expect([
group(halls, "halls-of-stone:tribunal-protectors").spawns.length,
group(halls, "halls-of-stone:tribunal-stormcallers").spawns.length,
group(halls, "halls-of-stone:tribunal-custodian").spawns.length,
]).toEqual([3, 2, 1]);
expect(incompleteObjective(halls, createDungeonEncounterSnapshot(adaptedEncounter(halls)), ["Krystallus", "Maiden"]))
.toBeDefined();
const pit = adaptedDefinition("pit-of-saron");
expect(group(pit, "pit-of-saron:first-ambush").spawns).toHaveLength(10);
expect(group(pit, "pit-of-saron:second-ambush-normal").spawns).toHaveLength(6);
expect(group(pit, "pit-of-saron:tyrannus").spawns).toHaveLength(1);
expect(pit.staticSpawns.map((spawn) => spawn.id)).toEqual(expect.arrayContaining([
objective(pit, "Garfrost").id,
objective(pit, "Ick").id,
]));
const pinnacle = adaptedDefinition("utgarde-pinnacle");
expect(group(pinnacle, "utgarde-pinnacle:svala-ritual-channelers").spawns).toHaveLength(3);
expect(group(pinnacle, "utgarde-pinnacle:skadi-first-wave").spawns).toHaveLength(13);
expect(group(pinnacle, "utgarde-pinnacle:skadi-reinforcement-one").spawns).toHaveLength(3);
expect(group(pinnacle, "utgarde-pinnacle:skadi-reinforcement-two").spawns).toHaveLength(3);
expect(pinnacle.staticSpawns.map((spawn) => spawn.id)).toContain(objective(pinnacle, "Ymiron").id);
const gundrak = adaptedDefinition("gundrak");
expect(group(gundrak, "gundrak:eck").spawns).toHaveLength(1);
expect(group(gundrak, "gundrak:eck").spawns[0].spawnMask).toBe(2);
expect(objective(gundrak, "Eck").optional).toBe(true);
expect(gundrak.staticSpawns.map((spawn) => spawn.id)).toEqual(expect.arrayContaining([
objective(gundrak, "Slad'ran").id,
objective(gundrak, "Moorabi").id,
objective(gundrak, "Drakkari Colossus").id,
objective(gundrak, "Gal'darah").id,
]));
});
it("keeps Eck's source cadence without turning setup and Berserk spells into repeat damage", () => {
const gundrak = adaptedDefinition("gundrak");
const eck = Object.values(gundrak.entities).find((entity) => entity.combat?.serverEntry === 29_932);
expect(eck?.name).toBe("Eck the Ferocious");
expect(eck?.visual.model).toMatchObject({ groundOffset: 0, labelHeight: 7 });
const attacks = new Map(eck?.combat?.attacks.map((attack) => [attack.spellId ?? 0, attack]));
expect(attacks.get(55_813)).toMatchObject({
name: "Eck Bite", target: "primary", initialCooldownMs: [5_000, 5_000],
repeatCooldownMs: [8_000, 12_000], damageMultiplier: 1.35,
});
expect(attacks.get(55_814)).toMatchObject({
name: "Eck Spit", target: "primary", initialCooldownMs: [10_000, 37_000],
repeatCooldownMs: [8_000, 12_000], damageMultiplier: 1.35,
});
expect(attacks.get(55_815)).toMatchObject({
name: "Eck Spring", target: "random-party", initialCooldownMs: [10_000, 24_000],
repeatCooldownMs: [10_000, 24_000], damageMultiplier: 1.35,
telegraph: { shape: "circle", anchor: "target", warningMs: 750 },
});
expect(attacks.get(55_837)).toMatchObject({ target: "self", maximumCasts: 1, damageMultiplier: 0 });
expect(attacks.get(55_816)).toMatchObject({
target: "self", initialCooldownMs: [90_000, 90_000], maximumCasts: 1,
damageMultiplier: 0, effect: { kind: "aura", aura: "enrage" },
});
});
it("runs every required lifecycle through its reviewed gates and completes without renderer state", () => {
const halls = adaptedDefinition("halls-of-stone");
const hallsRun = finishEncounter(halls, adaptedEncounter(halls));
expectOrderedPhases(hallsRun.phases, [
"tribunal-protectors-active",
"tribunal-stormcallers-active",
"tribunal-custodian-active",
"sjonnir-ready",
"complete",
]);
expect(hallsRun.snapshot.worldObjectStates["halls-of-stone:reviewed-sjonnir-door"]).toBe("open");
const pit = adaptedDefinition("pit-of-saron");
const pitRun = finishEncounter(pit, adaptedEncounter(pit));
expectOrderedPhases(pitRun.phases, [
"gauntlet-ready",
"first-ambush-active",
"second-ambush-active",
"tunnel-ready",
"tyrannus-active",
"complete",
]);
expect(pitRun.snapshot.flags["gauntlet-complete"]).toBe(true);
expect(pitRun.snapshot.worldObjectStates["pit-of-saron:reviewed-portcullis"]).toBe("open");
const pinnacle = adaptedDefinition("utgarde-pinnacle");
const pinnacleRun = finishEncounter(pinnacle, adaptedEncounter(pinnacle));
expectOrderedPhases(pinnacleRun.phases, [
"svala-phase-one-active",
"svala-ritual-active",
"svala-phase-two-active",
"gortok-ready",
"stasis-worgen-active",
"stasis-furbolg-active",
"gortok-active",
"skadi-ready",
"skadi-first-wave-active",
"harpoon-one-ready",
"skadi-reinforcement-one-active",
"harpoon-two-ready",
"skadi-reinforcement-two-active",
"harpoon-three-ready",
"skadi-ground-active",
"ymiron-ready",
"complete",
]);
expect(pinnacleRun.snapshot.counters["harpoon-hits"]).toBe(3);
expect(pinnacleRun.snapshot.worldObjectStates["utgarde-pinnacle:reviewed-ymiron-door"]).toBe("open");
const gundrak = adaptedDefinition("gundrak");
const gundrakRun = finishEncounter(gundrak, adaptedEncounter(gundrak));
expectOrderedPhases(gundrakRun.phases, [
"altar-progression",
"bridge-activating",
"gal-darah-ready",
"complete",
]);
expect(gundrakRun.snapshot.counters["keys-in"]).toBe(3);
expect(gundrakRun.snapshot.flags["bridge-open"]).toBe(true);
expect(gundrakRun.snapshot.worldObjectStates["gundrak:reviewed-bridge"]).toBe("open");
expect(gundrakRun.snapshot.completedObjectiveIds).not.toContain(objective(gundrak, "Eck").id);
expect(gundrakRun.snapshot.activeSpawnGroupIds).toContain("gundrak:eck");
for (const [definition, result] of [
[halls, hallsRun],
[pit, pitRun],
[pinnacle, pinnacleRun],
[gundrak, gundrakRun],
] as const) {
expect(result.snapshot.flags["dungeon-complete"], definition.id).toBe(true);
expect(result.snapshot.completedObjectiveIds, definition.id).toEqual(expect.arrayContaining(
definition.bosses.filter((boss) => !boss.optional).map((boss) => boss.id),
));
}
});
it("recovers from both wipe scopes at every nonfinal checkpoint, including optional Eck", () => {
for (const id of TARGETS) {
const definition = adaptedDefinition(id);
const encounter = adaptedEncounter(definition);
const run = finishEncounter(definition, encounter);
expect(run.checkpoints, id).toHaveLength(EXPECTED_NONFINAL_CHECKPOINTS[id]);
for (const checkpoint of run.checkpoints) {
for (const scope of ["boss-wipe", "checkpoint"] as const) {
const reset = resetDungeonEncounterSnapshot(
encounter,
checkpoint,
scope,
checkpoint.phaseStartedAt + 10_000,
);
expect(reset.phase, `${id}:${checkpoint.checkpointId}:${scope}`).toBe("resume");
expect(reset.activeSpawnGroupIds).toEqual([]);
expect(reset.completedObjectiveIds).toEqual(checkpoint.completedObjectiveIds);
expect(reset.checkpointId).toBe(checkpoint.checkpointId);
expect(finishEncounter(definition, encounter, reset).snapshot.flags["dungeon-complete"]).toBe(true);
}
}
}
const gundrak = adaptedDefinition("gundrak");
const encounter = adaptedEncounter(gundrak);
let eckCheckpoint = killIds(
encounter,
createDungeonEncounterSnapshot(encounter),
[objective(gundrak, "Moorabi").id],
1,
);
eckCheckpoint = killIds(
encounter,
eckCheckpoint,
group(gundrak, "gundrak:eck").spawns.map((spawn) => spawn.id),
2,
);
expect(eckCheckpoint.checkpointId).toBe(objective(gundrak, "Eck").id);
for (const scope of ["boss-wipe", "checkpoint"] as const) {
const reset = resetDungeonEncounterSnapshot(encounter, eckCheckpoint, scope, 3);
expect(reset.completedObjectiveIds).toContain(objective(gundrak, "Eck").id);
expect(finishEncounter(gundrak, encounter, reset).snapshot.flags["dungeon-complete"]).toBe(true);
}
});
it("keeps completion order-safe when a static final boss is reported dead before its event gate", () => {
for (const id of ["halls-of-stone", "utgarde-pinnacle", "gundrak"] as const) {
const definition = adaptedDefinition(id);
const encounter = adaptedEncounter(definition);
const finalBoss = objective(definition, FINAL_BOSS_NAME[id]);
const premature = killIds(encounter, createDungeonEncounterSnapshot(encounter), [finalBoss.id], 1);
expect(premature.flags["dungeon-complete"], id).toBe(false);
const completed = finishEncounter(definition, encounter, premature).snapshot;
expect(completed.flags["dungeon-complete"], id).toBe(true);
if (id === "gundrak") expect(completed.flags["bridge-open"]).toBe(true);
}
const pit = adaptedDefinition("pit-of-saron");
const pitEncounter = adaptedEncounter(pit);
const tyrannusGroup = group(pit, "pit-of-saron:tyrannus");
const prematureTyrannus = killIds(
pitEncounter,
createDungeonEncounterSnapshot(pitEncounter),
tyrannusGroup.spawns.map((spawn) => spawn.id),
1,
);
expect(prematureTyrannus.completedObjectiveIds).not.toContain(objective(pit, "Tyrannus").id);
expect(prematureTyrannus.phase).toBe("progression");
});
});