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> = 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> = Object.freeze({ "halls-of-stone": ["Tribunal of Ages"], "pit-of-saron": ["Tyrannus"], "utgarde-pinnacle": ["Svala", "Gortok", "Skadi"], gundrak: ["Eck"], }); const FINAL_BOSS_NAME: Readonly> = Object.freeze({ "halls-of-stone": "Sjonnir", "pit-of-saron": "Tyrannus", "utgarde-pinnacle": "Ymiron", gundrak: "Gal'darah", }); const EXPECTED_NONFINAL_CHECKPOINTS: Readonly> = 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 = 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> = { "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(); 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, objectIds: ReadonlySet, ): 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, objectIds: ReadonlySet, objectiveIds: ReadonlySet, ): 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, objectsById: ReadonlyMap, objectiveIds: ReadonlySet, ): 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"); }); });