Files
healer-man/src/game/wowClassicEncounterBatchThreeCatalog.test.ts
2026-09-09 11:21:33 -04:00

516 lines
30 KiB
TypeScript

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<string>();
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<string>();
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");
});
});