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 { DungeonEncounterDefinition, DungeonEncounterEvent, DungeonEncounterSnapshot, } from "./dungeonEncounterTypes"; import type { DungeonBossObjective, DungeonDefinition } from "./dungeonTypes"; import { validateDungeonWorldObject } from "./dungeonWorldObjects"; import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed"; import { WOW_WRATH_ENCOUNTER_BATCH_THREE_REVIEWS, withWowWrathEncounterBatchThreeContent, type WowWrathEncounterBatchThreeDungeonId, } from "./wowWrathEncounterBatchThreeCatalog"; const TARGETS = [ "utgarde-keep", "the-nexus", "draktharon-keep", "azjol-nerub", "halls-of-lightning", "ahnkahet-old-kingdom", "forge-of-souls", ] as const satisfies readonly WowWrathEncounterBatchThreeDungeonId[]; const REVIEWED_TEMPLATES: Readonly> = Object.freeze({ "utgarde-keep": [186_756, 186_694], "the-nexus": [188_526, 188_527, 188_528], "draktharon-keep": [189_299, 189_300, 189_301, 189_302], "azjol-nerub": [192_395, 192_396, 192_397, 192_398], "halls-of-lightning": [191_325, 191_326], "ahnkahet-old-kingdom": [192_236, 193_093, 193_094, 193_564], "forge-of-souls": [], }); const SUPPRESSED_ENTRIES: Readonly> = Object.freeze({ "utgarde-keep": [24_200, 24_201, 23_954], "the-nexus": [26_731, 26_763, 26_723], "draktharon-keep": [26_631, 26_632], "azjol-nerub": [28_684, 28_921, 29_120, 28_729, 28_730, 28_731], "halls-of-lightning": [28_546], "ahnkahet-old-kingdom": [29_309, 29_308, 29_310, 29_311], "forge-of-souls": [36_497], }); 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: WowWrathEncounterBatchThreeDungeonId): DungeonDefinition { const source = generatedDefinitions.find((definition) => definition.id === id); if (!source) throw new Error(`Missing generated dungeon ${id}`); return source; } function adaptedDefinition(id: WowWrathEncounterBatchThreeDungeonId): DungeonDefinition { return withWowWrathEncounterBatchThreeContent(sourceDefinition(id)); } function encounter(definition: DungeonDefinition): DungeonEncounterDefinition { const value = definition.encounters?.find((candidate) => candidate.id === `${definition.id}:wrath-batch-three-instance`); if (!value) throw new Error(`Missing batch-three encounter for ${definition.id}`); return value; } 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 send( definition: DungeonEncounterDefinition, snapshot: DungeonEncounterSnapshot, event: DungeonEncounterEvent, difficultyId: "normal" | "heroic" = "normal", ): DungeonEncounterSnapshot { return reduceDungeonEncounter(definition, snapshot, event, { difficultyId }).snapshot; } function killGroup( definition: DungeonEncounterDefinition, snapshot: DungeonEncounterSnapshot, actorIds: readonly string[], now: number, difficultyId: "normal" | "heroic" = "normal", ): DungeonEncounterSnapshot { return send(definition, snapshot, { kind: "tick", now, facts: { registeredEntityIds: actorIds, deadEntityIds: actorIds }, }, difficultyId); } function assertRecoverable( definition: DungeonEncounterDefinition, snapshot: DungeonEncounterSnapshot, now: number, difficultyId: "normal" | "heroic" = "normal", ): DungeonEncounterSnapshot { const completed = snapshot.completedObjectiveIds; const checkpoint = snapshot.checkpointId; expect(checkpoint).not.toBeNull(); let recovered = resetDungeonEncounterSnapshot(definition, snapshot, "boss-wipe", now); expect(recovered.phase).toBe("resume"); recovered = send(definition, recovered, { kind: "tick", now: now + 1 }, difficultyId); expect(recovered.phase).not.toBe("resume"); expect(recovered.completedObjectiveIds).toEqual(completed); expect(recovered.checkpointId).toBe(checkpoint); return recovered; } 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)), ]; } describe("Wrath encounter batch three source contract", () => { it("pins every reviewed AzerothCore source file by SHA-256", async () => { expect(WOW_WRATH_ENCOUNTER_BATCH_THREE_REVIEWS.map((review) => review.dungeonId)).toEqual(TARGETS); for (const review of WOW_WRATH_ENCOUNTER_BATCH_THREE_REVIEWS) { expect(review.fidelity).toBe("source-backed-adapted"); expect(review.adaptations.length).toBeGreaterThan(0); expect(review.remainingGaps.length).toBeGreaterThan(0); for (const sourceFile of review.sourceFiles) { expect(sourceFile.anchors.length).toBeGreaterThan(0); const bytes = await readFile(sourceFile.path); expect(createHash("sha256").update(bytes).digest("hex"), sourceFile.path).toBe(sourceFile.sha256); } } }); it("is idempotent and keeps all definitions, references, and source-row replacements valid", () => { for (const id of TARGETS) { const source = sourceDefinition(id); const adapted = adaptedDefinition(id); const controller = encounter(adapted); expect(withWowWrathEncounterBatchThreeContent(adapted)).toBe(adapted); expect(controller.source?.scope).toBe("instance-orchestration"); validateDungeonEncounterDefinition(controller); adapted.serverGameObjects.forEach(validateDungeonWorldObject); const objectiveIds = adapted.bosses.map((boss) => boss.id); expect(new Set(objectiveIds).size, `${id} objective ids`).toBe(objectiveIds.length); const orderedObjectiveIds = adapted.objectiveOrder.map((boss) => boss.id); expect(new Set(orderedObjectiveIds).size, `${id} objective-order ids`).toBe(orderedObjectiveIds.length); const spawnIds = runtimeSpawnIds(adapted); expect(new Set(spawnIds).size, `${id} runtime spawn ids`).toBe(spawnIds.length); const objectIds = adapted.serverGameObjects.map((object) => object.id); expect(new Set(objectIds).size, `${id} object ids`).toBe(objectIds.length); const sourceObjectSpawnIds = adapted.serverGameObjects .map((object) => object.source?.spawnId) .filter((spawnId): spawnId is number => spawnId !== undefined); expect(new Set(sourceObjectSpawnIds).size, `${id} source object rows`).toBe(sourceObjectSpawnIds.length); for (const templateId of REVIEWED_TEMPLATES[id]) { const originalRows = source.serverGameObjects.filter((object) => object.source?.templateId === templateId); const adaptedRows = adapted.serverGameObjects.filter((object) => object.source?.templateId === templateId); expect(adaptedRows.length, `${id} template ${templateId}`).toBe(originalRows.length); expect(adaptedRows.map((object) => object.source?.spawnId)).toEqual(originalRows.map((object) => object.source?.spawnId)); expect(adaptedRows.every((object) => object.source?.fidelity === "source-backed-adapted")).toBe(true); expect(adaptedRows.some((object) => originalRows.some((original) => original.id === object.id))).toBe(false); } const remainingEntries = new Set(adapted.staticSpawns.map((spawn) => adapted.entities[spawn.entityId]?.combat?.serverEntry)); for (const entry of SUPPRESSED_ENTRIES[id]) expect(remainingEntries.has(entry), `${id} static entry ${entry}`).toBe(false); const spawnGroupIds = new Set((adapted.encounterSpawnGroups ?? []).map((spawnGroup) => spawnGroup.id)); const worldObjectIds = new Set(adapted.serverGameObjects.map((object) => object.id)); const validObjectiveIds = new Set(adapted.bosses.map((boss) => boss.id)); const validEntityIds = new Set(runtimeSpawnIds(adapted)); for (const rule of controller.rules) { if (rule.trigger.kind === "entity-death" || rule.trigger.kind === "entity-engaged" || rule.trigger.kind === "entity-health") { expect(validEntityIds.has(rule.trigger.entityId), `${id} trigger entity ${rule.trigger.entityId}`).toBe(true); } if (rule.trigger.kind === "entities-dead") { rule.trigger.entityIds.forEach((entityId) => expect(validEntityIds.has(entityId), `${id} dead-set entity ${entityId}`).toBe(true)); } if (rule.trigger.kind === "interact") expect(worldObjectIds.has(rule.trigger.objectId), `${id} interaction ${rule.trigger.objectId}`).toBe(true); for (const action of rule.actions) { if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") { expect(spawnGroupIds.has(action.spawnGroupId), `${id} action group ${action.spawnGroupId}`).toBe(true); } if (action.kind === "set-world-object-state") expect(worldObjectIds.has(action.objectId), `${id} action object ${action.objectId}`).toBe(true); if (action.kind === "complete-objective") expect(validObjectiveIds.has(action.objectiveId), `${id} action objective ${action.objectiveId}`).toBe(true); } } } }); }); describe("Wrath encounter batch three lifecycle", () => { it("runs Utgarde Keep's order-safe duo ghosts and timed Ingvar resurrection with wipe recovery", () => { const dungeon = adaptedDefinition("utgarde-keep"); const controller = encounter(dungeon); const keleseth = objective(dungeon, "Keleseth"); const duo = objective(dungeon, "Skarvold"); const ingvar = objective(dungeon, "Ingvar"); const skarvald = group(dungeon, "utgarde-keep:skarvald"); const dalronn = group(dungeon, "utgarde-keep:dalronn"); const skarvaldGhost = group(dungeon, "utgarde-keep:skarvald-ghost"); const dalronnGhost = group(dungeon, "utgarde-keep:dalronn-ghost"); const ingvarLiving = group(dungeon, "utgarde-keep:ingvar-living"); const ingvarUndead = group(dungeon, "utgarde-keep:ingvar-undead"); let now = 0; let snapshot = createDungeonEncounterSnapshot(controller, now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: keleseth.id, now: ++now }); expect(snapshot.phase).toBe("duo-active"); expect(snapshot.activeSpawnGroupIds).toEqual(expect.arrayContaining([skarvald.id, dalronn.id])); snapshot = assertRecoverable(controller, snapshot, ++now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: dalronn.spawns[0].id, now: ++now }); expect(snapshot.flags["dalronn-dead"]).toBe(true); expect(snapshot.activeSpawnGroupIds).toContain(dalronnGhost.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: skarvald.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(duo.id); expect(snapshot.activeSpawnGroupIds).not.toContain(skarvaldGhost.id); expect(snapshot.activeSpawnGroupIds).not.toContain(dalronnGhost.id); expect(snapshot.phase).toBe("ingvar-living-active"); snapshot = assertRecoverable(controller, snapshot, ++now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: ingvarLiving.spawns[0].id, now: ++now }); expect(snapshot.phase).toBe("ingvar-resurrection"); const resurrectionStarted = now; snapshot = send(controller, snapshot, { kind: "tick", now: resurrectionStarted + 19_999 }); expect(snapshot.phase).toBe("ingvar-resurrection"); snapshot = send(controller, snapshot, { kind: "tick", now: resurrectionStarted + 20_000 }); expect(snapshot.phase).toBe("ingvar-undead-active"); expect(snapshot.activeSpawnGroupIds).toContain(ingvarUndead.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: ingvarUndead.spawns[0].id, now: resurrectionStarted + 20_001 }); expect(snapshot.completedObjectiveIds).toEqual(expect.arrayContaining([keleseth.id, duo.id, ingvar.id])); expect(snapshot.worldObjectStates["utgarde-keep:reviewed-portcullis-one"]).toBe("open"); expect(snapshot.worldObjectStates["utgarde-keep:reviewed-portcullis-two"]).toBe("open"); snapshot = send(controller, snapshot, { kind: "tick", now: resurrectionStarted + 20_002 }); expect(snapshot.phase).toBe("complete"); let alternate = createDungeonEncounterSnapshot(controller, 0); alternate = send(controller, alternate, { kind: "entity-death", entityId: keleseth.id, now: 1 }); alternate = send(controller, alternate, { kind: "entity-death", entityId: skarvald.spawns[0].id, now: 2 }); expect(alternate.activeSpawnGroupIds).toContain(skarvaldGhost.id); alternate = send(controller, alternate, { kind: "entity-death", entityId: dalronn.spawns[0].id, now: 3 }); expect(alternate.completedObjectiveIds).toContain(duo.id); }); it("keeps the source combat schedules on Utgarde Keep's ghost and undead replacement actors", () => { const dungeon = adaptedDefinition("utgarde-keep"); const entityForGroup = (groupId: string) => { const entityId = group(dungeon, groupId).spawns[0].entityId; return dungeon.entities[entityId]; }; const attacksForGroup = (groupId: string) => { return entityForGroup(groupId).combat?.attacks.filter((attack) => attack.id !== "basic-attack") ?? []; }; expect(entityForGroup("utgarde-keep:dalronn-ghost").combat?.attacks[0]).toMatchObject({ id: "basic-attack", name: "Basic Attack", delivery: "melee", school: "physical", animation: "attack", range: 4.5, }); expect(attacksForGroup("utgarde-keep:skarvald-ghost").map((attack) => ({ id: attack.id, spellId: attack.spellId, initial: attack.initialCooldownMs, repeat: attack.repeatCooldownMs, }))).toEqual([ { id: "charge-43651", spellId: 43_651, initial: [5_000, 5_000], repeat: [5_000, 10_000] }, { id: "stone-strike-48583", spellId: 48_583, initial: [10_000, 10_000], repeat: [5_000, 10_000] }, ]); expect(attacksForGroup("utgarde-keep:dalronn-ghost").map((attack) => ({ id: attack.id, spellId: attack.spellId, initial: attack.initialCooldownMs, repeat: attack.repeatCooldownMs, }))).toEqual([ { id: "shadow-bolt-43649", spellId: 43_649, initial: [1_000, 1_000], repeat: [2_000, 2_000] }, { id: "debilitate-43650", spellId: 43_650, initial: [5_000, 5_000], repeat: [5_000, 10_000] }, ]); expect(attacksForGroup("utgarde-keep:dalronn-ghost")[1].effect) .toEqual({ kind: "target-aura", aura: "debilitate", durationMs: 8_000 }); expect(attacksForGroup("utgarde-keep:ingvar-undead").map((attack) => ({ id: attack.id, spellId: attack.spellId, initial: attack.initialCooldownMs, repeat: attack.repeatCooldownMs, }))).toEqual([ { id: "dreadful-roar-42729", spellId: 42_729, initial: [15_000, 15_000], repeat: [15_000, 20_000] }, { id: "woe-strike-42730", spellId: 42_730, initial: [2_000, 2_000], repeat: [3_000, 7_000] }, { id: "dark-smash-42723", spellId: 42_723, initial: [5_000, 5_000], repeat: [9_000, 11_000] }, { id: "shadow-axe-42749", spellId: 42_749, initial: [10_000, 10_000], repeat: [35_000, 35_000] }, ]); expect(attacksForGroup("utgarde-keep:ingvar-undead").find((attack) => ( attack.id === "woe-strike-42730" ))?.school).toBe("physical"); expect(attacksForGroup("utgarde-keep:ingvar-undead").find((attack) => ( attack.id === "shadow-axe-42749" ))?.school).toBe("physical"); const review = WOW_WRATH_ENCOUNTER_BATCH_THREE_REVIEWS.find((candidate) => ( candidate.dungeonId === "utgarde-keep" ))!; expect(review.remainingGaps.join(" ")).toContain("Woe Strike's damage-on-heal reaction"); expect(review.remainingGaps.join(" ")).toContain("Shadow Axe's outbound/returning actor"); }); it("runs Nexus spheres, normal Telestra split, Keristrasza release, and checkpoint recovery", () => { const dungeon = adaptedDefinition("the-nexus"); const controller = encounter(dungeon); const anomalus = objective(dungeon, "Anomalus"); const ormorok = objective(dungeon, "Ormorok"); const telestra = objective(dungeon, "Telestra"); const keristrasza = objective(dungeon, "Keristrasza"); const opening = group(dungeon, "the-nexus:telestra-opening"); const anomalusOpening = group(dungeon, "the-nexus:anomalus-opening"); const anomalusRift = group(dungeon, "the-nexus:anomalus-empowered-rift"); const anomalusFinal = group(dungeon, "the-nexus:anomalus-final"); const clones = group(dungeon, "the-nexus:telestra-first-clones"); const normalFinal = group(dungeon, "the-nexus:telestra-normal-final"); const keristraszaActor = group(dungeon, "the-nexus:keristrasza"); let now = 0; let snapshot = createDungeonEncounterSnapshot(controller, now); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(opening.id); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: anomalusOpening.spawns[0].id, healthPercent: 51, now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(anomalusRift.id); snapshot = killGroup(controller, snapshot, anomalusRift.spawns.map((spawn) => spawn.id), ++now); expect(snapshot.activeSpawnGroupIds).toContain(anomalusFinal.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: anomalusFinal.spawns[0].id, now: ++now }); expect(snapshot.worldObjectStates["the-nexus:reviewed-anomalus-sphere"]).toBe("ready"); snapshot = assertRecoverable(controller, snapshot, ++now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: ormorok.id, now: ++now }); expect(snapshot.worldObjectStates["the-nexus:reviewed-ormorok-sphere"]).toBe("ready"); snapshot = assertRecoverable(controller, snapshot, ++now); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: opening.spawns[0].id, healthPercent: 51, now: ++now }); expect(snapshot.phase).toBe("telestra-first-clones"); snapshot = killGroup(controller, snapshot, clones.spawns.map((spawn) => spawn.id), ++now); expect(snapshot.phase).toBe("telestra-normal-final"); expect(snapshot.activeSpawnGroupIds).toContain(normalFinal.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: normalFinal.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(telestra.id); expect(snapshot.worldObjectStates["the-nexus:reviewed-telestra-sphere"]).toBe("ready"); snapshot = assertRecoverable(controller, snapshot, ++now); for (const sphere of [ "the-nexus:reviewed-telestra-sphere", "the-nexus:reviewed-anomalus-sphere", "the-nexus:reviewed-ormorok-sphere", ]) snapshot = send(controller, snapshot, { kind: "interact", objectId: sphere, now: ++now }); expect(snapshot.counters["spheres-used"]).toBe(3); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(keristraszaActor.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: keristraszaActor.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(keristrasza.id); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.phase).toBe("complete"); }); it("selects Telestra's second split only on heroic", () => { const dungeon = adaptedDefinition("the-nexus"); const controller = encounter(dungeon); const opening = group(dungeon, "the-nexus:telestra-opening"); const firstClones = group(dungeon, "the-nexus:telestra-first-clones"); const middle = group(dungeon, "the-nexus:telestra-heroic-middle"); const secondClones = group(dungeon, "the-nexus:telestra-second-clones"); const final = group(dungeon, "the-nexus:telestra-heroic-final"); let snapshot = createDungeonEncounterSnapshot(controller, 0); snapshot = send(controller, snapshot, { kind: "tick", now: 1 }, "heroic"); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: opening.spawns[0].id, healthPercent: 51, now: 2 }, "heroic"); snapshot = killGroup(controller, snapshot, firstClones.spawns.map((spawn) => spawn.id), 3, "heroic"); expect(snapshot.phase).toBe("telestra-heroic-middle"); expect(snapshot.activeSpawnGroupIds).toContain(middle.id); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: middle.spawns[0].id, healthPercent: 11, now: 4 }, "heroic"); expect(snapshot.activeSpawnGroupIds).toContain(secondClones.id); snapshot = killGroup(controller, snapshot, secondClones.spawns.map((spawn) => spawn.id), 5, "heroic"); expect(snapshot.phase).toBe("telestra-heroic-final"); expect(snapshot.activeSpawnGroupIds).toContain(final.id); }); it("runs Drak'Tharon's four crystal waves and Tharon'ja stage with each checkpoint recoverable", () => { const dungeon = adaptedDefinition("draktharon-keep"); const controller = encounter(dungeon); const trollgore = objective(dungeon, "Trollgore"); const novos = objective(dungeon, "Novos"); const dred = objective(dungeon, "King Dred"); const tharon = objective(dungeon, "Tharon'ja"); const novosActor = group(dungeon, "draktharon-keep:novos"); const tharonOpening = group(dungeon, "draktharon-keep:tharon-opening"); const tharonFlesh = group(dungeon, "draktharon-keep:tharon-flesh"); let now = 0; let snapshot = createDungeonEncounterSnapshot(controller, now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: trollgore.id, now: ++now }); expect(snapshot.phase).toBe("novos-handler-1"); snapshot = assertRecoverable(controller, snapshot, ++now); for (let waveIndex = 1; waveIndex <= 4; waveIndex += 1) { const wave = group(dungeon, `draktharon-keep:novos-handler-wave-${waveIndex}`); snapshot = killGroup(controller, snapshot, wave.spawns.map((spawn) => spawn.id), ++now); expect(snapshot.worldObjectStates[`draktharon-keep:reviewed-novos-crystal-${waveIndex}`]).toBe("complete"); } expect(snapshot.phase).toBe("novos-active"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: novosActor.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(novos.id); snapshot = assertRecoverable(controller, snapshot, ++now); expect(snapshot.phase).toBe("dred-active"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: dred.id, now: ++now }); snapshot = assertRecoverable(controller, snapshot, ++now); expect(snapshot.activeSpawnGroupIds).toContain(tharonOpening.id); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: tharonOpening.spawns[0].id, healthPercent: 55, now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(tharonFlesh.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: tharonFlesh.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toEqual(expect.arrayContaining([trollgore.id, novos.id, dred.id, tharon.id])); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.phase).toBe("complete"); }); }); function advanceAzjolToAnubarak(difficultyId: "normal" | "heroic") { const dungeon = adaptedDefinition("azjol-nerub"); const controller = encounter(dungeon); const krik = objective(dungeon, "Krik'thir"); const hadronox = objective(dungeon, "Hadronox"); const watchers = group(dungeon, "azjol-nerub:krikthir-watchers"); const krikActor = group(dungeon, "azjol-nerub:krikthir"); const hadronoxActor = group(dungeon, "azjol-nerub:hadronox"); let now = 0; let snapshot = createDungeonEncounterSnapshot(controller, now); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }, difficultyId); snapshot = killGroup(controller, snapshot, watchers.spawns.map((spawn) => spawn.id), ++now, difficultyId); expect(snapshot.activeSpawnGroupIds).toContain(krikActor.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: krikActor.spawns[0].id, now: ++now }, difficultyId); expect(snapshot.completedObjectiveIds).toContain(krik.id); snapshot = assertRecoverable(controller, snapshot, ++now, difficultyId); expect(snapshot.worldObjectStates["azjol-nerub:reviewed-krikthir-passage"]).toBe("open"); snapshot = send(controller, snapshot, { kind: "interact", objectId: "azjol-nerub:hadronox-start", now: ++now }, difficultyId); for (let waveIndex = 1; waveIndex <= 3; waveIndex += 1) { const wave = group(dungeon, `azjol-nerub:crusher-wave-${waveIndex}`); snapshot = killGroup(controller, snapshot, wave.spawns.map((spawn) => spawn.id), ++now, difficultyId); } snapshot = send(controller, snapshot, { kind: "entity-death", entityId: hadronoxActor.spawns[0].id, now: ++now }, difficultyId); expect(snapshot.completedObjectiveIds).toContain(hadronox.id); snapshot = assertRecoverable(controller, snapshot, ++now, difficultyId); snapshot = send(controller, snapshot, { kind: "interact", objectId: "azjol-nerub:anubarak-start", now: ++now }, difficultyId); expect(snapshot.phase).toBe("anubarak-stage-1"); for (let doorIndex = 1; doorIndex <= 3; doorIndex += 1) { expect(snapshot.worldObjectStates[`azjol-nerub:reviewed-anubarak-door-${doorIndex}`]).toBe("closed"); } return { dungeon, controller, snapshot, now }; } describe("Wrath encounter batch three route gates", () => { it("runs Azjol-Nerub's normal watcher, crusher, and three-submerge route", () => { const prepared = advanceAzjolToAnubarak("normal"); const { dungeon, controller } = prepared; const anub = objective(dungeon, "Anub'arak"); let { snapshot, now } = prepared; const thresholds = [75, 50, 25] as const; for (let index = 0; index < thresholds.length; index += 1) { const stage = group(dungeon, `azjol-nerub:anubarak-stage-${index + 1}`); const adds = group(dungeon, `azjol-nerub:anubarak-intermission-${index + 1}-normal`); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: stage.spawns[0].id, healthPercent: thresholds[index], now: ++now, }); expect(snapshot.activeSpawnGroupIds).toContain(adds.id); expect(adds.spawns.length).toBe([7, 8, 13][index]); snapshot = killGroup(controller, snapshot, adds.spawns.map((spawn) => spawn.id), ++now); expect(snapshot.phase).toBe(`anubarak-stage-${index + 2}`); } const finalStage = group(dungeon, "azjol-nerub:anubarak-stage-4"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: finalStage.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(anub.id); for (let doorIndex = 1; doorIndex <= 3; doorIndex += 1) { expect(snapshot.worldObjectStates[`azjol-nerub:reviewed-anubarak-door-${doorIndex}`]).toBe("open"); } snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.phase).toBe("complete"); }); it("selects every larger Anub'arak intermission on heroic", () => { const prepared = advanceAzjolToAnubarak("heroic"); const { dungeon, controller } = prepared; let { snapshot, now } = prepared; const thresholds = [75, 50, 25] as const; for (let index = 0; index < thresholds.length; index += 1) { const stage = group(dungeon, `azjol-nerub:anubarak-stage-${index + 1}`); const heroicAdds = group(dungeon, `azjol-nerub:anubarak-intermission-${index + 1}-heroic`); const normalAdds = group(dungeon, `azjol-nerub:anubarak-intermission-${index + 1}-normal`); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: stage.spawns[0].id, healthPercent: thresholds[index], now: ++now, }, "heroic"); expect(snapshot.activeSpawnGroupIds).toContain(heroicAdds.id); expect(snapshot.activeSpawnGroupIds).not.toContain(normalAdds.id); expect(heroicAdds.spawns.length).toBe([8, 10, 15][index]); expect(heroicAdds.spawns.every((spawn) => spawn.spawnMask === 2)).toBe(true); snapshot = killGroup(controller, snapshot, heroicAdds.spawns.map((spawn) => spawn.id), ++now, "heroic"); } expect(snapshot.phase).toBe("anubarak-stage-4"); }); it("runs Halls of Lightning's timed Ionar split and mandatory watcher checkpoint", () => { const dungeon = adaptedDefinition("halls-of-lightning"); const controller = encounter(dungeon); const bjarngrim = objective(dungeon, "Bjarngrim"); const volkhan = objective(dungeon, "Volkhan"); const ionar = objective(dungeon, "Ionar"); const loken = objective(dungeon, "Loken"); const opening = group(dungeon, "halls-of-lightning:ionar-opening"); const sparks = group(dungeon, "halls-of-lightning:ionar-sparks"); const final = group(dungeon, "halls-of-lightning:ionar-final"); const watchers = group(dungeon, "halls-of-lightning:awakened-watchers"); let now = 0; let snapshot = createDungeonEncounterSnapshot(controller, now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: bjarngrim.id, now: ++now }); snapshot = assertRecoverable(controller, snapshot, ++now); expect(snapshot.phase).toBe("volkhan-active"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: volkhan.id, now: ++now }); expect(snapshot.worldObjectStates["halls-of-lightning:reviewed-volkhan-door"]).toBe("open"); snapshot = assertRecoverable(controller, snapshot, ++now); expect(snapshot.activeSpawnGroupIds).toContain(opening.id); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: opening.spawns[0].id, healthPercent: 50, now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(sparks.id); const splitStarted = now; snapshot = send(controller, snapshot, { kind: "tick", now: splitStarted + 19_999 }); expect(snapshot.phase).toBe("ionar-sparks-active"); snapshot = send(controller, snapshot, { kind: "tick", now: splitStarted + 20_000 }); expect(snapshot.activeSpawnGroupIds).toContain(final.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: final.spawns[0].id, now: splitStarted + 20_001 }); expect(snapshot.completedObjectiveIds).toContain(ionar.id); expect(snapshot.activeSpawnGroupIds).toContain(watchers.id); snapshot = assertRecoverable(controller, snapshot, splitStarted + 20_002); expect(snapshot.activeSpawnGroupIds).toContain(watchers.id); snapshot = killGroup(controller, snapshot, watchers.spawns.map((spawn) => spawn.id), splitStarted + 20_003); expect(snapshot.flags["watchers-cleared"]).toBe(true); expect(snapshot.phase).toBe("loken-active"); snapshot = assertRecoverable(controller, snapshot, splitStarted + 20_004); expect(snapshot.phase).toBe("loken-active"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: loken.id, now: splitStarted + 20_005 }); snapshot = send(controller, snapshot, { kind: "tick", now: splitStarted + 20_006 }); expect(snapshot.phase).toBe("complete"); }); it("runs Ahn'kahet's spheres, guardian, ritual, insanity stages, and recovers each boss checkpoint", () => { const dungeon = adaptedDefinition("ahnkahet-old-kingdom"); const controller = encounter(dungeon); const nadox = objective(dungeon, "Nadox"); const taldaram = objective(dungeon, "Taldaram"); const jedoga = objective(dungeon, "Jedoga"); const volazj = objective(dungeon, "Volazj"); const nadoxOpening = group(dungeon, "ahnkahet-old-kingdom:nadox-opening"); const nadoxGuardian = group(dungeon, "ahnkahet-old-kingdom:nadox-guardian"); const nadoxFinal = group(dungeon, "ahnkahet-old-kingdom:nadox-final"); const taldaramActor = group(dungeon, "ahnkahet-old-kingdom:taldaram"); const initiates = group(dungeon, "ahnkahet-old-kingdom:jedoga-initiates"); const jedogaOpening = group(dungeon, "ahnkahet-old-kingdom:jedoga-opening"); const volunteer = group(dungeon, "ahnkahet-old-kingdom:jedoga-volunteer"); const jedogaFinal = group(dungeon, "ahnkahet-old-kingdom:jedoga-final"); let now = 0; let snapshot = createDungeonEncounterSnapshot(controller, now); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); for (let sphereIndex = 1; sphereIndex <= 2; sphereIndex += 1) { snapshot = send(controller, snapshot, { kind: "interact", objectId: `ahnkahet-old-kingdom:reviewed-sphere-${sphereIndex}`, now: ++now }); } expect(snapshot.counters["spheres-used"]).toBe(2); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: nadoxOpening.spawns[0].id, healthPercent: 50, now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(nadoxGuardian.id); snapshot = killGroup(controller, snapshot, nadoxGuardian.spawns.map((spawn) => spawn.id), ++now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: nadoxFinal.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(nadox.id); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(taldaramActor.id); expect(snapshot.worldObjectStates["ahnkahet-old-kingdom:reviewed-taldaram-platform"]).toBe("open"); snapshot = assertRecoverable(controller, snapshot, ++now); expect(snapshot.activeSpawnGroupIds).toContain(taldaramActor.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: taldaramActor.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(taldaram.id); snapshot = assertRecoverable(controller, snapshot, ++now); expect(snapshot.activeSpawnGroupIds).toContain(initiates.id); snapshot = killGroup(controller, snapshot, initiates.spawns.map((spawn) => spawn.id), ++now); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: jedogaOpening.spawns[0].id, healthPercent: 55, now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(volunteer.id); snapshot = killGroup(controller, snapshot, volunteer.spawns.map((spawn) => spawn.id), ++now); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: jedogaFinal.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toContain(jedoga.id); snapshot = assertRecoverable(controller, snapshot, ++now); for (const [index, threshold] of [66, 33].entries()) { const stage = group(dungeon, `ahnkahet-old-kingdom:volazj-stage-${index + 1}`); const visages = group(dungeon, `ahnkahet-old-kingdom:volazj-visages-${index + 1}`); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: stage.spawns[0].id, healthPercent: threshold, now: ++now }); expect(snapshot.activeSpawnGroupIds).toContain(visages.id); snapshot = killGroup(controller, snapshot, visages.spawns.map((spawn) => spawn.id), ++now); } const volazjFinal = group(dungeon, "ahnkahet-old-kingdom:volazj-stage-3"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: volazjFinal.spawns[0].id, now: ++now }); expect(snapshot.completedObjectiveIds).toEqual(expect.arrayContaining([nadox.id, taldaram.id, jedoga.id, volazj.id])); snapshot = send(controller, snapshot, { kind: "tick", now: ++now }); expect(snapshot.phase).toBe("complete"); const amanitarEntry = dungeon.staticSpawns .filter((spawn) => dungeon.entities[spawn.entityId]?.combat?.serverEntry === 30_258); expect(amanitarEntry).toHaveLength(1); expect(amanitarEntry[0].spawnMask).toBe(2); }); it("keeps Forge of Souls doorless while staging Bronjahm's 35-percent Soulstorm", () => { const dungeon = adaptedDefinition("forge-of-souls"); const controller = encounter(dungeon); const bronjahm = objective(dungeon, "Bronjahm"); const devourer = objective(dungeon, "Devourer"); const opening = group(dungeon, "forge-of-souls:bronjahm-opening"); const soulstorm = group(dungeon, "forge-of-souls:bronjahm-soulstorm"); let snapshot = createDungeonEncounterSnapshot(controller, 0); snapshot = send(controller, snapshot, { kind: "tick", now: 1 }); expect(snapshot.activeSpawnGroupIds).toContain(opening.id); snapshot = send(controller, snapshot, { kind: "entity-health", entityId: opening.spawns[0].id, healthPercent: 35, now: 2 }); expect(snapshot.activeSpawnGroupIds).toContain(soulstorm.id); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: soulstorm.spawns[0].id, now: 3 }); expect(snapshot.completedObjectiveIds).toContain(bronjahm.id); snapshot = assertRecoverable(controller, snapshot, 4); expect(snapshot.phase).toBe("devourer-active"); snapshot = send(controller, snapshot, { kind: "entity-death", entityId: devourer.id, now: 5 }); snapshot = send(controller, snapshot, { kind: "tick", now: 6 }); expect(snapshot.phase).toBe("complete"); expect(REVIEWED_TEMPLATES["forge-of-souls"]).toEqual([]); }); });