import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { describe, expect, it } from "vitest"; import { createDungeonEncounterSnapshot, reduceDungeonEncounter, resetDungeonEncounterSnapshot, validateDungeonEncounterDefinition, } from "./dungeonEncounterRuntime"; import type { DungeonEncounterDefinition, DungeonEncounterSnapshot, } from "./dungeonEncounterTypes"; import { DUNGEON_REGISTRY } from "./dungeonRegistry"; import type { DungeonDefinition } from "./dungeonTypes"; import { validateDungeonWorldObject } from "./dungeonWorldObjects"; import { WOW_WRATH_ENCOUNTER_CONTENT_REVIEWS, withWowWrathEncounterContent, } from "./wowWrathEncounterContentCatalog"; const TARGETS = [ "halls-of-reflection", "the-oculus", "trial-of-the-champion", ] as const; const START_OBJECT_BY_DUNGEON = Object.freeze({ "halls-of-reflection": "halls-of-reflection:wave-start", "the-oculus": "the-oculus:expedition-start", "trial-of-the-champion": "trial-of-the-champion:announcer-start", }); function adaptedDefinition(id: typeof TARGETS[number]): DungeonDefinition { const source = DUNGEON_REGISTRY[id]; if (!source) throw new Error(`Missing generated dungeon ${id}`); return withWowWrathEncounterContent(source); } function adaptedEncounter(definition: DungeonDefinition): DungeonEncounterDefinition { const encounter = definition.encounters?.find((candidate) => candidate.id === `${definition.id}:wrath-instance`); if (!encounter) throw new Error(`Missing Wrath encounter for ${definition.id}`); return encounter; } function activeSpawnIds( definition: DungeonDefinition, snapshot: DungeonEncounterSnapshot, ): readonly string[] { const active = new Set(snapshot.activeSpawnGroupIds); return (definition.encounterSpawnGroups ?? []) .filter((group) => active.has(group.id)) .flatMap((group) => group.spawns.map((spawn) => spawn.id)); } function advanceOneStep( definition: DungeonDefinition, encounter: DungeonEncounterDefinition, snapshot: DungeonEncounterSnapshot, now: number, ): DungeonEncounterSnapshot { if (snapshot.phase === "ready") { return reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: START_OBJECT_BY_DUNGEON[definition.id as keyof typeof START_OBJECT_BY_DUNGEON], now, }).snapshot; } if (snapshot.phase === "escape-ready") { return reduceDungeonEncounter(encounter, snapshot, { kind: "interact", objectId: "halls-of-reflection:escape-start", now, }).snapshot; } const spawnIds = activeSpawnIds(definition, snapshot); if (spawnIds.length) { return reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now, facts: { registeredEntityIds: spawnIds, deadEntityIds: spawnIds }, }).snapshot; } return reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: Math.max(now, snapshot.phaseStartedAt + 1_000_000), facts: { registeredEntityIds: [], deadEntityIds: [] }, }).snapshot; } function finishEncounter( definition: DungeonDefinition, encounter: DungeonEncounterDefinition, initial = createDungeonEncounterSnapshot(encounter), ): { readonly snapshot: DungeonEncounterSnapshot; readonly phases: readonly string[] } { let snapshot = initial; const phases = [snapshot.phase]; let now = snapshot.phaseStartedAt + 1; for (let step = 0; step < 80 && 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 === 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 }; } 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); } } describe("source-reviewed Wrath instance adapters", () => { it("keeps immutable file hashes and source anchors beside every adapter", async () => { expect(WOW_WRATH_ENCOUNTER_CONTENT_REVIEWS.map((review) => review.dungeonId)).toEqual(TARGETS); for (const review of WOW_WRATH_ENCOUNTER_CONTENT_REVIEWS) { expect(review.fidelity).toBe("source-backed-adapted"); expect(review.sourceFiles.length).toBeGreaterThanOrEqual(3); 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(createHash("sha256").update(await readFile(source.path)).digest("hex"), source.path) .toBe(source.sha256); } } }); it("validates definitions, group references, safety timeouts, and reviewed blocker geometry", () => { for (const id of TARGETS) { const source = DUNGEON_REGISTRY[id]; const definition = adaptedDefinition(id); const encounter = adaptedEncounter(definition); validateDungeonEncounterDefinition(encounter); definition.serverGameObjects.forEach(validateDungeonWorldObject); const groupIds = new Set((definition.encounterSpawnGroups ?? []).map((group) => group.id)); const objectIds = new Set(definition.serverGameObjects.map((object) => object.id)); for (const rule of encounter.rules) { if (rule.trigger.kind === "entities-dead") { expect(rule.trigger.timeoutMs, `${id}:${rule.id}`).toBeGreaterThan(0); } for (const action of rule.actions) { if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") { expect(groupIds.has(action.spawnGroupId), `${id}:${action.id}`).toBe(true); } if (action.kind === "set-world-object-state") { expect(objectIds.has(action.objectId), `${id}:${action.id}`).toBe(true); } } } const reviewedBlockers = definition.serverGameObjects.filter((object) => ( object.id.includes(":reviewed-") || object.id.includes(":ice-wall-") )); expect(reviewedBlockers.length).toBeGreaterThan(0); for (const blocker of reviewedBlockers) { const closed = blocker.states.find((state) => state.id === "closed"); const open = blocker.states.find((state) => state.id === "open"); expect(closed).toMatchObject({ visible: true, blocksMovement: true }); expect(open).toMatchObject({ blocksMovement: false }); } const originalImportedObjects = definition.serverGameObjects.filter((object) => ( object.source?.spawnId !== undefined && !object.id.includes(":reviewed-") )); const originalImportedObjectIds = new Set(originalImportedObjects.map((object) => object.id)); for (const object of source.serverGameObjects.filter((candidate) => ( candidate.source?.spawnId !== undefined && !candidate.id.includes(":reviewed-") ))) { expect(originalImportedObjectIds.has(object.id), `${id} dropped imported object ${object.id}`).toBe(true); } expect(originalImportedObjects.every((object) => object.visual?.hidden === true)).toBe(true); expect(originalImportedObjects.every((object) => object.states.every((state) => !state.blocksMovement))).toBe(true); expect(withWowWrathEncounterContent(definition)).toBe(definition); } }); it("removes every unsafe static event boss and preserves exact sourced stage cardinality", () => { const expectations = { "halls-of-reflection": [38_112, 38_113], "the-oculus": [27_447, 27_654, 27_655, 27_656], "trial-of-the-champion": [30_188, 35_451, 35_569, 35_571, 35_572], } as const; for (const id of TARGETS) { const definition = adaptedDefinition(id); const staticEntries = definition.staticSpawns.map((spawn) => definition.entities[spawn.entityId]?.combat?.serverEntry); for (const entry of expectations[id]) expect(staticEntries).not.toContain(entry); expect(definition.bosses.every((boss) => boss.eventOnly)).toBe(true); const staticSpawnIds = new Set(definition.staticSpawns.map((spawn) => spawn.id)); for (const boss of definition.bosses) { expect(staticSpawnIds.has(boss.id), `${id} left event boss ${boss.name} statically spawned`).toBe(false); } } const halls = adaptedDefinition("halls-of-reflection"); expect([1, 2, 3, 4, 6, 7, 8, 9].map((wave) => ( halls.encounterSpawnGroups?.find((group) => group.id === `halls-of-reflection:wave-${String(wave).padStart(2, "0")}`)?.spawns.length ))).toEqual([3, 3, 4, 4, 5, 5, 5, 5]); const oculus = adaptedDefinition("the-oculus"); expect(oculus.encounterSpawnGroups?.find((group) => group.id === "the-oculus:centrifuge-constructs")?.spawns).toHaveLength(10); expect([1, 2, 3].map((wave) => ( oculus.encounterSpawnGroups?.find((group) => group.id === `the-oculus:urom-menagerie-${wave}`)?.spawns.length ))).toEqual([4, 4, 4]); const trial = adaptedDefinition("trial-of-the-champion"); expect([1, 2, 3].map((wave) => ( trial.encounterSpawnGroups?.find((group) => group.id.startsWith(`trial-of-the-champion:minions-${wave}-`))?.spawns.length ))).toEqual([3, 3, 3]); expect(trial.encounterSpawnGroups?.find((group) => group.id === "trial-of-the-champion:argent-soldiers")?.spawns).toHaveLength(9); expect([1, 2, 3].map((phase) => ( trial.encounterSpawnGroups?.find((group) => group.id === `trial-of-the-champion:black-knight-phase-${phase}`)?.spawns.length ))).toEqual([1, 1, 1]); }); it("gives every Trial event boss its pinned phase-specific combat schedule", () => { const trial = adaptedDefinition("trial-of-the-champion"); const trialReview = WOW_WRATH_ENCOUNTER_CONTENT_REVIEWS.find( (review) => review.dungeonId === "trial-of-the-champion", ); expect(trialReview?.adaptations.join(" ")).toContain( "six-second, one-charge +100% critical chance modifier", ); const entity = (id: string) => trial.entities[id]; const specials = (id: string) => entity(id).combat?.attacks.filter((attack) => attack.id !== "basic-attack") ?? []; for (const id of [ "wrath-event-mounted-mokra", "wrath-event-mounted-eressea", "wrath-event-mounted-runok", ]) { expect(specials(id).map((attack) => attack.id)).toEqual([ "mounted-charge-63010", "shield-breaker-68504", "thrust-68505", ]); expect(specials(id).map((attack) => [attack.initialCooldownMs, attack.repeatCooldownMs])).toEqual([ [[2_500, 4_000], [4_500, 6_000]], [[5_000, 8_000], [6_000, 8_000]], [[3_000, 5_000], [3_000, 5_000]], ]); } expect(specials("wrath-event-eadric").map((attack) => attack.spellId)).toEqual([ 66_865, 66_935, 66_867, ]); expect(specials("wrath-event-eadric").find((attack) => attack.id === "vengeance-66865")?.effect) .toEqual({ kind: "aura", aura: "vengeance", durationMs: 0, meleeAutoCriticalProc: { chance: 1, triggeredAura: "vengeance-next-melee-critical-66889", durationMs: 6_000, criticalChancePercent: 100, charges: 1, }, }); expect(specials("wrath-event-eadric").find((attack) => attack.id === "radiance-66935")?.repeatCooldownMs) .toEqual([16_000, 16_000]); expect(specials("wrath-event-eadric").find((attack) => attack.id === "radiance-66935")) .toMatchObject({ school: "holy", damageMultiplier: 0.9, effect: { kind: "control", mechanic: "confuse", durationMs: 2_000, damageMultiplier: 0.9 }, }); expect(specials("wrath-event-eadric").find((attack) => attack.id === "hammer-of-justice-and-righteous-66863-66867")) .toMatchObject({ name: "Hammer of Justice + Hammer of the Righteous", target: "random-party", initialCooldownMs: [25_000, 25_000], repeatCooldownMs: [25_000, 25_000], effect: { kind: "control", mechanic: "stun", durationMs: 6_000, damageMultiplier: 1.3 }, }); expect(specials("wrath-event-eadric").some((attack) => attack.id === "hammer-of-justice-66863")).toBe(false); for (const id of ["wrath-event-mounted-eressea", "wrath-event-black-knight-phase-3"]) { expect(entity(id).combat?.attacks[0]).toMatchObject({ id: "basic-attack", name: "Basic Attack", delivery: "melee", school: "physical", animation: "attack", range: 4.5, }); } expect(specials("wrath-event-black-knight-phase-1").map((attack) => attack.id)).toEqual([ "plague-strike-67724", "icy-touch-67718", "death-respite-67745", "obliterate-67725", ]); expect(specials("wrath-event-black-knight-phase-1")[0]).toMatchObject({ id: "plague-strike-67724", school: "physical", }); expect(specials("wrath-event-black-knight-phase-2").map((attack) => attack.id)).toEqual([ "plague-strike-67724", "icy-touch-67718", "obliterate-67725", "desecration-67778", ]); expect(specials("wrath-event-black-knight-phase-2")[0]).toMatchObject({ id: "plague-strike-67724", school: "physical", }); expect(specials("wrath-event-black-knight-phase-3").map((attack) => ({ id: attack.id, initial: attack.initialCooldownMs, repeat: attack.repeatCooldownMs, }))).toEqual([ { id: "death-bite-67808", initial: [2_000, 2_000], repeat: [2_000, 4_000] }, { id: "marked-for-death-67823", initial: [1_000, 1_000], repeat: [9_000, 9_000] }, ]); expect(specials("wrath-event-black-knight-phase-3")[1].effect) .toEqual({ kind: "target-aura", aura: "marked-for-death", durationMs: 9_000 }); }); it("reduces every complete source-ordered lifecycle without renderer state", () => { const halls = adaptedDefinition("halls-of-reflection"); const hallsRun = finishEncounter(halls, adaptedEncounter(halls)); expectOrderedPhases(hallsRun.phases, [ "wave-01-active", "wave-05-active", "wave-06-source-pause", "wave-06-active", "wave-10-active", "general-active", "escape-ready", "escape-1-active", "escape-4-active", "complete", ]); const oculus = adaptedDefinition("the-oculus"); const oculusRun = finishEncounter(oculus, adaptedEncounter(oculus)); expectOrderedPhases(oculusRun.phases, [ "drakos-active", "constructs-active", "varos-active", "urom-menagerie-1-active", "urom-menagerie-3-active", "urom-active", "eregos-active", "complete", ]); const trial = adaptedDefinition("trial-of-the-champion"); const trialRun = finishEncounter(trial, adaptedEncounter(trial)); expectOrderedPhases(trialRun.phases, [ "champion-minions-1-active", "champion-minions-3-active", "mounted-champions-active", "foot-champions-active", "argent-soldiers-active", "eadric-active", "black-knight-phase-1-active", "black-knight-phase-3-active", "complete", ]); for (const [definition, result] of [ [halls, hallsRun], [oculus, oculusRun], [trial, trialRun], ] as const) { expect(result.snapshot.flags["dungeon-complete"]).toBe(true); expect(result.snapshot.completedObjectiveIds).toEqual( expect.arrayContaining(definition.bosses.filter((boss) => !boss.optional).map((boss) => boss.id)), ); expect(result.snapshot.activeSpawnGroupIds).toEqual([]); } }); it("recovers from every non-final persistent checkpoint without a soft lock", () => { const expectedCheckpointCounts = { "halls-of-reflection": 3, "the-oculus": 3, "trial-of-the-champion": 2, } as const; for (const id of TARGETS) { const definition = adaptedDefinition(id); const encounter = adaptedEncounter(definition); let snapshot = createDungeonEncounterSnapshot(encounter); let now = 1; const checkpointSnapshots: DungeonEncounterSnapshot[] = []; const seenCheckpoints = new Set(); for (let step = 0; step < 80 && snapshot.phase !== "complete"; step += 1) { snapshot = advanceOneStep(definition, encounter, snapshot, now); now = Math.max(now + 1, snapshot.phaseStartedAt + 1); if (snapshot.checkpointId && !seenCheckpoints.has(snapshot.checkpointId) && snapshot.phase !== "complete") { seenCheckpoints.add(snapshot.checkpointId); checkpointSnapshots.push(snapshot); } } expect(checkpointSnapshots, id).toHaveLength(expectedCheckpointCounts[id]); for (const checkpoint of checkpointSnapshots) { const reset = resetDungeonEncounterSnapshot(encounter, checkpoint, "boss-wipe", now + 1); expect(reset.phase).toBe("resume"); expect(reset.activeSpawnGroupIds).toEqual([]); expect(reset.completedObjectiveIds).toEqual(checkpoint.completedObjectiveIds); const resumed = reduceDungeonEncounter(encounter, reset, { kind: "tick", now: now + 2 }).snapshot; expect(resumed.phase).not.toBe("resume"); expect( resumed.activeSpawnGroupIds.length > 0 || resumed.phase === "escape-ready", `${id}:${checkpoint.checkpointId}`, ).toBe(true); expect(finishEncounter(definition, encounter, resumed).snapshot.flags["dungeon-complete"]).toBe(true); } } }); });