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([[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([]); }); });