import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { gunzipSync, strFromU8 } from "fflate"; import { describe, expect, it } from "vitest"; import { createDungeonEncounterSnapshot, reduceDungeonEncounter, resetDungeonEncounterSnapshot, validateDungeonEncounterDefinition, } from "./dungeonEncounterRuntime"; import type { DungeonDefinition, DungeonEncounterSpawnGroupDefinition } from "./dungeonTypes"; import type { DungeonEncounterDefinition, DungeonEncounterSnapshot } from "./dungeonEncounterTypes"; import { validateDungeonWorldObject } from "./dungeonWorldObjects"; import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed"; import { withWowEncounterContent } from "./wowEncounterContentCatalog"; import { WOW_CLASSIC_ENCOUNTER_BATCH_THREE_REVIEWS, withWowClassicEncounterBatchThree, } from "./wowClassicEncounterBatchThreeCatalog"; 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 original(id: string): DungeonDefinition { const definition = campaign.definitions.find((candidate) => candidate.id === id); if (!definition) throw new Error(`Missing generated dungeon ${id}`); return definition; } function preadapted(id: string): DungeonDefinition { return withWowEncounterContent(original(id)); } function adapted(id: string): DungeonDefinition { return withWowClassicEncounterBatchThree(preadapted(id)); } function encounter(definition: DungeonDefinition): DungeonEncounterDefinition { const value = definition.encounters?.find((candidate) => candidate.id === `${definition.id}:classic-batch-three`); if (!value) throw new Error(`Missing Classic batch-three encounter for ${definition.id}`); return value; } function group(definition: DungeonDefinition, id: string): DungeonEncounterSpawnGroupDefinition { const value = definition.encounterSpawnGroups?.find((candidate) => candidate.id === id); if (!value) throw new Error(`Missing encounter group ${id}`); return value; } function objectBySpawn(definition: DungeonDefinition, spawnId: number) { const matches = definition.serverGameObjects.filter((object) => object.source?.spawnId === spawnId); if (matches.length !== 1) throw new Error(`${definition.id} expected one source object ${spawnId}, found ${matches.length}`); return matches[0]; } function killGroup( scripted: DungeonEncounterDefinition, snapshot: DungeonEncounterSnapshot, spawnGroup: DungeonEncounterSpawnGroupDefinition, now: number, ): DungeonEncounterSnapshot { const ids = spawnGroup.spawns.map((spawn) => spawn.id); return reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now, facts: { registeredEntityIds: ids, deadEntityIds: ids }, }).snapshot; } function staticEntries(definition: DungeonDefinition): readonly number[] { return definition.staticSpawns.flatMap((spawn) => { const entry = definition.entities[spawn.entityId]?.combat?.serverEntry; return entry === undefined ? [] : [entry]; }); } describe("Classic source-backed encounter batch three", () => { it("pins sources, validates references, suppresses event actors, remains idempotent, and preserves Solakar", () => { const cases = [ ["gnomeregan", [7_998]], ["dire-maul", [11_496]], ["blackrock-spire", [9_816, 10_316, 10_429, 10_264]], ] as const; expect(WOW_CLASSIC_ENCOUNTER_BATCH_THREE_REVIEWS.map((review) => review.dungeonId)) .toEqual(cases.map(([id]) => id)); const checkedPaths = new Set(); for (const review of WOW_CLASSIC_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.sha256).toMatch(/^[a-f0-9]{64}$/); expect(sourceFile.anchors.length).toBeGreaterThan(0); expect(sourceFile.anchors.every((anchor) => /lines? \d+/.test(anchor))).toBe(true); if (!checkedPaths.has(sourceFile.path)) { expect(createHash("sha256").update(readFileSync(resolve(sourceFile.path))).digest("hex"), sourceFile.path) .toBe(sourceFile.sha256); checkedPaths.add(sourceFile.path); } } } const priorBRS = preadapted("blackrock-spire"); const priorSolakarEncounter = priorBRS.encounters?.find((value) => value.id === "blackrock-spire:wow-instance"); const priorSolakarGroups = priorBRS.encounterSpawnGroups?.filter((value) => value.id.startsWith("blackrock-spire:solakar")); const priorFatherFlame = priorBRS.serverGameObjects.find((value) => value.source?.templateId === 175_245); expect(priorSolakarEncounter).toBeDefined(); expect(priorSolakarGroups).toHaveLength(6); expect(priorFatherFlame).toBeDefined(); for (const [id, suppressed] of cases) { const definition = adapted(id); const scripted = encounter(definition); validateDungeonEncounterDefinition(scripted); expect(scripted.source?.scope).toBe("instance-orchestration"); expect(withWowClassicEncounterBatchThree(definition)).toBe(definition); expect(staticEntries(definition)).not.toEqual(expect.arrayContaining([...suppressed])); const spawnIds = new Set([ ...definition.staticSpawns.map((spawn) => spawn.id), ...(definition.encounterSpawnGroups ?? []).flatMap((spawnGroup) => spawnGroup.spawns.map((spawn) => spawn.id)), ]); const groupIds = new Set(definition.encounterSpawnGroups?.map((spawnGroup) => spawnGroup.id)); const objectIds = new Set(definition.serverGameObjects.map((object) => object.id)); const objectiveIds = new Set(definition.bosses.map((objective) => objective.id)); expect(objectIds.size).toBe(definition.serverGameObjects.length); expect(groupIds.size).toBe(definition.encounterSpawnGroups?.length ?? 0); const actionIds = new Set(); for (const spawnGroup of definition.encounterSpawnGroups ?? []) { for (const spawn of spawnGroup.spawns) expect(definition.entities[spawn.entityId], `${id}:${spawn.id}`).toBeDefined(); } for (const rule of scripted.rules) { if (rule.trigger.kind === "interact") expect(objectIds.has(rule.trigger.objectId), `${id}:${rule.id}`).toBe(true); if (rule.trigger.kind === "entity-death" || rule.trigger.kind === "entity-engaged" || rule.trigger.kind === "entity-health") { expect(spawnIds.has(rule.trigger.entityId), `${id}:${rule.id}:${rule.trigger.entityId}`).toBe(true); } if (rule.trigger.kind === "entities-dead") { expect(rule.trigger.entityIds.every((entityId) => spawnIds.has(entityId)), `${id}:${rule.id}`).toBe(true); } for (const action of rule.actions) { expect(actionIds.has(action.id), `${id}:duplicate action ${action.id}`).toBe(false); actionIds.add(action.id); if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") { expect(groupIds.has(action.spawnGroupId), `${id}:${rule.id}:${action.id}`).toBe(true); } if (action.kind === "set-world-object-state") { const object = definition.serverGameObjects.find((candidate) => candidate.id === action.objectId); expect(object, `${id}:${rule.id}:${action.id}`).toBeDefined(); expect(object?.states.some((state) => state.id === action.state), `${id}:${rule.id}:${action.id}`).toBe(true); } if (action.kind === "complete-objective") { expect(objectiveIds.has(action.objectiveId), `${id}:${rule.id}:${action.id}`).toBe(true); } } } const sourceSpawnIds = definition.serverGameObjects.flatMap((object) => ( object.source?.spawnId === undefined ? [] : [object.source.spawnId] )); expect(new Set(sourceSpawnIds).size).toBe(sourceSpawnIds.length); for (const object of definition.serverGameObjects.filter((candidate) => ( scripted.initialState?.worldObjectStates?.[candidate.id] !== undefined ))) { validateDungeonWorldObject(object); expect(object.visual?.hidden).not.toBe(true); } expect(definition.validation.warnings.some((warning) => warning.includes("remain"))).toBe(true); } const brs = adapted("blackrock-spire"); expect(brs.encounters?.find((value) => value.id === "blackrock-spire:wow-instance")).toEqual(priorSolakarEncounter); expect(brs.encounterSpawnGroups?.filter((value) => value.id.startsWith("blackrock-spire:solakar"))) .toEqual(priorSolakarGroups); expect(brs.serverGameObjects.find((value) => value.source?.templateId === 175_245)).toEqual(priorFatherFlame); }); it("ports Gyth and Urok's pinned combat schedules instead of leaving event bosses basic-only", () => { const definition = adapted("blackrock-spire"); const byEntry = (entry: number) => Object.values(definition.entities) .find((entity) => entity.combat?.serverEntry === entry)!; const gyth = byEntry(10_339); const urok = byEntry(10_584); expect(gyth.combat?.attacks.filter((attack) => attack.id !== "basic-attack").map((attack) => ({ id: attack.id, spellId: attack.spellId, initial: attack.initialCooldownMs, repeat: attack.repeatCooldownMs, }))).toEqual([ { id: "corrosive-acid-16359", spellId: 16_359, initial: [8_000, 16_000], repeat: [10_000, 16_000] }, { id: "freeze-16350", spellId: 16_350, initial: [8_000, 16_000], repeat: [10_000, 16_000] }, { id: "flame-breath-16390", spellId: 16_390, initial: [8_000, 16_000], repeat: [10_000, 16_000] }, { id: "knock-away-10101", spellId: 10_101, initial: [12_000, 18_000], repeat: [14_000, 20_000] }, ]); expect(gyth.combat?.mechanicIds).toEqual([ "corrosive-acid-16359", "freeze-16350", "flame-breath-16390", "knock-away-10101", ]); expect(gyth.combat?.attacks.find((attack) => attack.id === "corrosive-acid-16359")) .toMatchObject({ target: "nearby-party", school: "nature", range: 20, radius: 20, telegraph: { shape: "cone", anchor: "source", length: 20, angleDegrees: 90 }, }); expect(gyth.combat?.attacks.find((attack) => attack.id === "freeze-16350")) .toMatchObject({ target: "nearby-party", school: "frost", range: 20, radius: 20, effect: { kind: "control", mechanic: "stun", durationMs: 10_000, damageMultiplier: 0.8 }, telegraph: { shape: "cone", anchor: "source", length: 20, angleDegrees: 90 }, }); expect(gyth.combat?.attacks.find((attack) => attack.id === "flame-breath-16390")) .toMatchObject({ target: "nearby-party", school: "fire", range: 20, radius: 20, telegraph: { shape: "cone", anchor: "source", length: 20, angleDegrees: 90 }, }); expect(urok.combat?.attacks.filter((attack) => attack.id !== "basic-attack").map((attack) => ({ id: attack.id, spellId: attack.spellId, initial: attack.initialCooldownMs, repeat: attack.repeatCooldownMs, }))).toEqual([ { id: "rend-16509", spellId: 16_509, initial: [17_000, 20_000], repeat: [8_000, 10_000] }, { id: "strike-15580", spellId: 15_580, initial: [10_000, 12_000], repeat: [8_000, 10_000] }, { id: "intimidating-roar-16508", spellId: 16_508, initial: [25_000, 30_000], repeat: [40_000, 45_000] }, ]); expect(urok.combat?.attacks.find((attack) => attack.id === "intimidating-roar-16508")) .toMatchObject({ target: "nearby-party", range: 8, radius: 8, effect: { kind: "control", mechanic: "fear", durationMs: 8_000 }, telegraph: { shape: "circle", anchor: "source", radius: 8 }, }); const review = WOW_CLASSIC_ENCOUNTER_BATCH_THREE_REVIEWS.find((candidate) => ( candidate.dungeonId === "blackrock-spire" ))!; expect(review.remainingGaps.join(" ")).toContain("Freeze periodic damage"); expect(review.remainingGaps.join(" ")).toContain("split primary-root/nearby-fear targeting"); }); it("ports Crystal Fang's exact death summon while keeping the source-faithful rare boss melee-only", () => { const definition = adapted("blackrock-spire"); const scripted = encounter(definition); const crystalFang = definition.staticSpawns.find((spawn) => ( definition.entities[spawn.entityId]?.combat?.serverEntry === 10_376 ))!; const spiderling = group(definition, "blackrock-spire:batch-three:crystal-fang-spiderling"); expect(crystalFang.spawnChance).toBe(0.15); expect(spiderling.spawns).toHaveLength(1); expect(spiderling.spawns[0]).toMatchObject({ entityId: expect.stringContaining("entry-10375"), position: crystalFang.position, }); const snapshot = reduceDungeonEncounter(scripted, createDungeonEncounterSnapshot(scripted), { kind: "entity-death", entityId: crystalFang.id, now: 1, }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(spiderling.id); }); it("runs Gnomeregan's exact Emi group counts, cave-ins, retry, doors, and persistent Grubbis gate", () => { const definition = adapted("gnomeregan"); const scripted = encounter(definition); const groups = ["ambushers-1", "ambushers-2", "ambushers-3a", "ambushers-3b", "grubbis"] .map((name) => group(definition, `gnomeregan:batch-three:${name}`)); expect(groups.map((value) => value.spawns.length)).toEqual([10, 4, 5, 5, 2]); expect(groups[2].spawns.map((spawn) => spawn.position)).toEqual(groups[3].spawns.map((spawn) => spawn.position)); const emi = definition.serverGameObjects.find((object) => object.id === "gnomeregan:batch-three:emi-shortfuse")!; const caveOne = objectBySpawn(definition, 6_447); const caveTwo = objectBySpawn(definition, 17_633); const workshop = objectBySpawn(definition, 12_003); const finalDoor = objectBySpawn(definition, 6_910); let snapshot = createDungeonEncounterSnapshot(scripted); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: workshop.id, now: 0 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: finalDoor.id, now: 1 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: emi.id, now: 2 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(groups[0].id); snapshot = killGroup(scripted, snapshot, groups[0], 3); snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 4); expect(snapshot).toMatchObject({ phase: "idle", activeSpawnGroupIds: [] }); expect(snapshot.worldObjectStates[emi.id]).toBe("ready"); expect(snapshot.worldObjectStates[caveTwo.id]).toBe("closed"); expect(snapshot.worldObjectStates[workshop.id]).toBe("open"); expect(snapshot.worldObjectStates[finalDoor.id]).toBe("open"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: emi.id, now: 10 }).snapshot; snapshot = killGroup(scripted, snapshot, groups[0], 11); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 5_011 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(groups[1].id); snapshot = killGroup(scripted, snapshot, groups[1], 5_012); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 45_012 }).snapshot; expect(snapshot.worldObjectStates[caveTwo.id]).toBe("open"); expect(snapshot.activeSpawnGroupIds).toContain(groups[2].id); snapshot = killGroup(scripted, snapshot, groups[2], 45_013); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 48_013 }).snapshot; snapshot = killGroup(scripted, snapshot, groups[3], 48_014); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 108_014 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(groups[4].id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: "gnomeregan:batch-three:grubbis", now: 108_015, }).snapshot; expect(snapshot.completedObjectiveIds).toContain("gnomeregan:batch-three:grubbis"); expect(snapshot.worldObjectStates[caveOne.id]).toBe("open"); expect(snapshot.worldObjectStates[emi.id]).toBe("complete"); expect(snapshot.checkpointId).toBe("gnomeregan-grubbis"); snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 108_016); expect(snapshot.completedObjectiveIds).toContain("gnomeregan:batch-three:grubbis"); expect(snapshot.worldObjectStates[caveOne.id]).toBe("open"); const fleet = definition.encounters?.find((value) => value.id === "gnomeregan:boss-progression")!; const completion = fleet.rules.find((rule) => rule.id === "all-required-bosses-defeated")!; expect(completion.guards).toContainEqual({ kind: "objective-complete", objectiveId: "gnomeregan:batch-three:grubbis", equals: true, }); }); it("runs Dire Maul's pylons, Immol'thar recovery, east-wing wall, doors, and six tribute tiers", () => { const definition = adapted("dire-maul"); const scripted = encounter(definition); const pylons = [29_742, 29_743, 29_744, 29_745, 29_746].map((id) => objectBySpawn(definition, id)); const forcefield = objectBySpawn(definition, 29_747); const courtyard = objectBySpawn(definition, 29_750); const inner = objectBySpawn(definition, 29_756); const conservatory = objectBySpawn(definition, 268_275); const wall = objectBySpawn(definition, 268_276); const ironbark = definition.serverGameObjects.find((object) => object.id === "dire-maul:batch-three:old-ironbark")!; const tribute = definition.serverGameObjects.find((object) => object.id === "dire-maul:batch-three:gordok-tribute")!; const immol = group(definition, "dire-maul:batch-three:immolthar"); const alzzin = group(definition, "dire-maul:batch-three:alzzin"); let snapshot = createDungeonEncounterSnapshot(scripted); pylons.forEach((pylon, index) => { snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: pylon.id, now: index }).snapshot; }); expect(snapshot.counters["pylons-off"]).toBe(5); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 10 }).snapshot; expect(snapshot.worldObjectStates[forcefield.id]).toBe("open"); expect(snapshot.activeSpawnGroupIds).toContain(immol.id); snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 11); expect(snapshot.activeSpawnGroupIds).not.toContain(immol.id); expect(snapshot.counters["pylons-off"]).toBe(5); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 12 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(immol.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: immol.spawns[0].id, now: 13 }).snapshot; expect(snapshot.completedObjectiveIds).toContain(immol.spawns[0].id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: "creature-248429", now: 20 }).snapshot; expect(snapshot.worldObjectStates[ironbark.id]).toBe("ready"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: ironbark.id, now: 21 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 7_021 }).snapshot; expect(snapshot.worldObjectStates[conservatory.id]).toBe("open"); expect(snapshot.activeSpawnGroupIds).toContain(alzzin.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-health", entityId: alzzin.spawns[0].id, healthPercent: 51, now: 7_022 }).snapshot; expect(snapshot.worldObjectStates[wall.id]).toBe("closed"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-health", entityId: alzzin.spawns[0].id, healthPercent: 50, now: 7_023 }).snapshot; expect(snapshot.worldObjectStates[wall.id]).toBe("open"); snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 7_024); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 7_025 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(alzzin.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: alzzin.spawns[0].id, now: 7_026 }).snapshot; expect(snapshot.completedObjectiveIds).toContain("dire-maul:batch-three:alzzin"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: courtyard.id, now: 8_000 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: inner.id, now: 8_001 }).snapshot; expect(snapshot.worldObjectStates[courtyard.id]).toBe("open"); expect(snapshot.worldObjectStates[inner.id]).toBe("open"); for (const id of ["creature-248079", "creature-248080"]) { snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: id, now: snapshot.revision + 8_010 }).snapshot; } snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: "creature-248076", now: 9_000 }).snapshot; expect(snapshot.counters["tribute-penalties"]).toBe(2); expect(snapshot.worldObjectStates[tribute.id]).toBe("tier-4"); let perfect = createDungeonEncounterSnapshot(scripted); perfect = reduceDungeonEncounter(scripted, perfect, { kind: "entity-death", entityId: "creature-248076", now: 1 }).snapshot; expect(perfect.worldObjectStates[tribute.id]).toBe("tier-6"); const optionalIds = new Set(["creature-248077", "creature-248079", "creature-248080", "creature-248081", "creature-248082"]); expect(definition.bosses.filter((boss) => optionalIds.has(boss.id)).every((boss) => boss.optional)).toBe(true); const fleetCompletion = definition.encounters?.find((value) => value.id === "dire-maul:boss-progression") ?.rules.find((rule) => rule.id === "all-required-bosses-defeated"); expect(fleetCompletion?.guards?.some((guard) => guard.kind === "objective-complete" && optionalIds.has(guard.objectiveId))) .toBe(false); }); it("runs Blackrock Spire's runes, Emberseer reset, exact stadium, Drakkisath doors, and Urok retry", () => { const definition = adapted("blackrock-spire"); const scripted = encounter(definition); const hallRunes = [87_827, 87_829, 87_825, 87_830, 87_828, 87_826, 87_824].map((id) => objectBySpawn(definition, id)); const emberRunes = [87_831, 87_832, 87_833, 87_834, 87_835, 87_836, 87_837].map((id) => objectBySpawn(definition, id)); const emberIn = objectBySpawn(definition, 87_839); const emberOut = objectBySpawn(definition, 87_841); const emberDoors = objectBySpawn(definition, 87_842); const emberTrigger = definition.serverGameObjects.find((object) => object.id === "blackrock-spire:batch-three:emberseer-trigger")!; const incarcerators = group(definition, "blackrock-spire:batch-three:incarcerators"); const emberseer = group(definition, "blackrock-spire:batch-three:emberseer"); const runeMobs = [ ["creature-137869", "creature-137871", "creature-137928", "creature-137930"], ["creature-137872", "creature-137896", "creature-137898"], ["creature-137870", "creature-137886", "creature-137929"], ["creature-137873", "creature-137892", "creature-137897", "creature-137903", "creature-137904", "creature-137905", "creature-137931", "creature-137934"], ["creature-137878", "creature-137911", "creature-137912", "creature-137939"], ["creature-137880", "creature-137881", "creature-137942", "creature-137943"], ["creature-137877", "creature-137908", "creature-137909", "creature-137910", "creature-137938"], ]; let snapshot = createDungeonEncounterSnapshot(scripted); runeMobs.forEach((ids, index) => { snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: index + 1, facts: { registeredEntityIds: ids, deadEntityIds: ids }, }).snapshot; }); expect(snapshot.counters["dragonspire-runes"]).toBe(7); expect(hallRunes.every((rune) => snapshot.worldObjectStates[rune.id] === "inactive")).toBe(true); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 8 }).snapshot; expect(snapshot.worldObjectStates[emberIn.id]).toBe("open"); expect(snapshot.worldObjectStates[emberDoors.id]).toBe("open"); expect(snapshot.worldObjectStates[emberTrigger.id]).toBe("ready"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: emberTrigger.id, now: 10 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 2_010 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(incarcerators.id); snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 2_011); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 2_012 }).snapshot; expect(snapshot.worldObjectStates[emberTrigger.id]).toBe("ready"); expect(snapshot.worldObjectStates[emberIn.id]).toBe("open"); expect(snapshot.activeSpawnGroupIds).not.toContain(incarcerators.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: emberTrigger.id, now: 3_000 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 5_000 }).snapshot; snapshot = killGroup(scripted, snapshot, incarcerators, 5_001); expect(emberRunes.every((rune) => snapshot.worldObjectStates[rune.id] === "growing")).toBe(true); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 30_001 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(emberseer.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: emberseer.spawns[0].id, now: 30_002 }).snapshot; expect(snapshot.completedObjectiveIds).toContain("creature-137817"); expect(snapshot.worldObjectStates[emberOut.id]).toBe("open"); expect(emberRunes.every((rune) => snapshot.worldObjectStates[rune.id] === "released")).toBe(true); const stadiumEntry = objectBySpawn(definition, 87_845); const stadiumWaveDoor = objectBySpawn(definition, 87_843); const stadiumExit = objectBySpawn(definition, 87_844); const waves = Array.from({ length: 6 }, (_, index) => group(definition, `blackrock-spire:batch-three:stadium-wave-${index + 1}`)); expect(waves.map((value) => value.spawns.length)).toEqual([4, 3, 4, 4, 5, 5]); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: stadiumEntry.id, now: 31_000 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 42_000 }).snapshot; for (let index = 0; index < waves.length; index += 1) { expect(snapshot.activeSpawnGroupIds).toContain(waves[index].id); snapshot = killGroup(scripted, snapshot, waves[index], snapshot.phaseStartedAt + 1); if (index < waves.length - 1) { snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: snapshot.phaseStartedAt + 13_000 }).snapshot; } } snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: snapshot.phaseStartedAt + 18_000 }).snapshot; const gyth = group(definition, "blackrock-spire:batch-three:gyth"); const rend = group(definition, "blackrock-spire:batch-three:rend"); expect(snapshot.activeSpawnGroupIds).toContain(gyth.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-health", entityId: gyth.spawns[0].id, healthPercent: 26, now: snapshot.phaseStartedAt + 1 }).snapshot; expect(snapshot.activeSpawnGroupIds).not.toContain(rend.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-health", entityId: gyth.spawns[0].id, healthPercent: 25, now: snapshot.phaseStartedAt + 2 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(rend.id); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: gyth.spawns[0].id, now: snapshot.phaseStartedAt + 3 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: rend.spawns[0].id, now: snapshot.phaseStartedAt + 4 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: snapshot.phaseStartedAt + 5 }).snapshot; expect(snapshot.completedObjectiveIds).toContain("creature-137968"); expect(snapshot.worldObjectStates[stadiumEntry.id]).toBe("complete"); expect(snapshot.worldObjectStates[stadiumWaveDoor.id]).toBe("open"); expect(snapshot.worldObjectStates[stadiumExit.id]).toBe("open"); const drakkDoors = [objectBySpawn(definition, 87_854), objectBySpawn(definition, 87_855)]; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: "creature-137971", now: snapshot.phaseStartedAt + 6 }).snapshot; expect(drakkDoors.every((door) => snapshot.worldObjectStates[door.id] === "open")).toBe(true); const pile = objectBySpawn(definition, 99_791); const urok = group(definition, "blackrock-spire:batch-three:urok"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: pile.id, now: snapshot.phaseStartedAt + 7 }).snapshot; expect(snapshot.activeSpawnGroupIds).toContain(urok.id); snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", snapshot.phaseStartedAt + 8); expect(snapshot.worldObjectStates[pile.id]).toBe("ready"); expect(snapshot.completedObjectiveIds).toContain("creature-137817"); expect(snapshot.completedObjectiveIds).toContain("creature-137968"); snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: pile.id, now: snapshot.phaseStartedAt + 9 }).snapshot; snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: urok.spawns[0].id, now: snapshot.phaseStartedAt + 10 }).snapshot; expect(snapshot.completedObjectiveIds).toContain("blackrock-spire:batch-three:urok-doomhowl"); expect(snapshot.worldObjectStates[pile.id]).toBe("complete"); }); });