Release Healer Man 0.1.22

This commit is contained in:
phenom committed 2026-09-09 11:21:33 -04:00
1 parent f6f2375145
commit 7f6001723a
144 files changed
+54107 -690

No files matched your search

@@ -0,0 +1,668 @@
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<Record<WowWrathEncounterBatchThreeDungeonId, readonly number[]>> = 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<Record<WowWrathEncounterBatchThreeDungeonId, readonly number[]>> = 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([]);
});
});