forked from phenom/healer-man
Release Healer Man 0.1.22
This commit is contained in:
1 parent
f6f2375145
commit
7f6001723a
144 files changed
+54107
-690
No files matched your search
@@ -0,0 +1,482 @@
|
||||
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 {
|
||||
WOW_CLASSIC_ENCOUNTER_BATCH_TWO_REVIEWS,
|
||||
withWowClassicEncounterBatchTwo,
|
||||
} from "./wowClassicEncounterBatchTwoCatalog";
|
||||
|
||||
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 adapted(id: string): DungeonDefinition {
|
||||
return withWowClassicEncounterBatchTwo(original(id));
|
||||
}
|
||||
|
||||
function encounter(definition: DungeonDefinition): DungeonEncounterDefinition {
|
||||
const value = definition.encounters?.find((candidate) => candidate.id === `${definition.id}:classic-batch-two`);
|
||||
if (!value) throw new Error(`Missing Classic batch-two 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(
|
||||
definition: DungeonEncounterDefinition,
|
||||
snapshot: DungeonEncounterSnapshot,
|
||||
spawnGroup: DungeonEncounterSpawnGroupDefinition,
|
||||
now: number,
|
||||
) {
|
||||
const entityIds = spawnGroup.spawns.map((spawn) => spawn.id);
|
||||
return reduceDungeonEncounter(definition, snapshot, {
|
||||
kind: "tick",
|
||||
now,
|
||||
facts: { registeredEntityIds: entityIds, deadEntityIds: entityIds },
|
||||
}).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 two", () => {
|
||||
it("pins exact source hashes, validates every reference, suppresses event actors, and stays idempotent", () => {
|
||||
const cases = [
|
||||
["sunken-temple", [8_580, 8_443, 5_709]],
|
||||
["blackrock-depths", [9_035, 9_038, 9_040, 9_037, 9_036, 9_034, 9_039]],
|
||||
["scholomance", [1_853]],
|
||||
["stratholme", [10_440]],
|
||||
] as const;
|
||||
expect(WOW_CLASSIC_ENCOUNTER_BATCH_TWO_REVIEWS.map((review) => review.dungeonId))
|
||||
.toEqual(cases.map(([id]) => id));
|
||||
|
||||
const verifiedPaths = new Set<string>();
|
||||
for (const review of WOW_CLASSIC_ENCOUNTER_BATCH_TWO_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 (!verifiedPaths.has(sourceFile.path)) {
|
||||
const actualHash = createHash("sha256").update(readFileSync(resolve(sourceFile.path))).digest("hex");
|
||||
expect(actualHash, sourceFile.path).toBe(sourceFile.sha256);
|
||||
verifiedPaths.add(sourceFile.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, suppressedEntries] of cases) {
|
||||
const definition = adapted(id);
|
||||
const scripted = encounter(definition);
|
||||
validateDungeonEncounterDefinition(scripted);
|
||||
expect(withWowClassicEncounterBatchTwo(definition)).toBe(definition);
|
||||
expect(staticEntries(definition)).not.toEqual(expect.arrayContaining([...suppressedEntries]));
|
||||
|
||||
const spawnIds = new Set([
|
||||
...definition.staticSpawns.map((spawn) => spawn.id),
|
||||
...(definition.encounterSpawnGroups ?? []).flatMap((spawnGroup) => spawnGroup.spawns.map((spawn) => spawn.id)),
|
||||
]);
|
||||
const spawnGroupIds = 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));
|
||||
for (const spawnGroup of definition.encounterSpawnGroups ?? []) {
|
||||
for (const spawn of spawnGroup.spawns) {
|
||||
expect(definition.entities[spawn.entityId], `${id}:${spawnGroup.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) {
|
||||
if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") {
|
||||
expect(spawnGroupIds.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);
|
||||
}
|
||||
});
|
||||
|
||||
it("runs Sunken Temple's ordered statues, ritual, defender gate, and Eranikus recovery", () => {
|
||||
const definition = adapted("sunken-temple");
|
||||
const scripted = encounter(definition);
|
||||
const statues = [27_898, 27_899, 28_111, 28_112, 28_113, 28_114]
|
||||
.map((spawnId) => objectBySpawn(definition, spawnId));
|
||||
const forcefield = objectBySpawn(definition, 268_932);
|
||||
const altar = objectBySpawn(definition, 11_922);
|
||||
const flames = [47_561, 47_562, 47_563, 47_564].map((spawnId) => objectBySpawn(definition, spawnId));
|
||||
let snapshot = createDungeonEncounterSnapshot(scripted);
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: statues[1].id, now: 1 }).snapshot;
|
||||
expect(snapshot.counters["statue-progress"]).toBe(0);
|
||||
for (let index = 0; index < statues.length; index += 1) {
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "interact",
|
||||
objectId: statues[index].id,
|
||||
now: index + 2,
|
||||
}).snapshot;
|
||||
}
|
||||
expect(snapshot).toMatchObject({ phase: "atal-alarion-active", counters: { "statue-progress": 6 } });
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("sunken-temple:atal-alarion");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 20);
|
||||
expect(snapshot).toMatchObject({ phase: "recovery", counters: { "statue-progress": 6 }, activeSpawnGroupIds: [] });
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 20 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("sunken-temple:atal-alarion");
|
||||
snapshot = killGroup(scripted, snapshot, group(definition, "sunken-temple:atal-alarion"), 21);
|
||||
const atalAlarion = definition.bosses.find((objective) => objective.name === "Atal'alarion")!;
|
||||
expect(snapshot.completedObjectiveIds).toContain(atalAlarion.id);
|
||||
|
||||
const defenderIds = ["creature-39843", "creature-39844", "creature-39845", "creature-39846", "creature-34522", "creature-39847"];
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "tick",
|
||||
now: 22,
|
||||
facts: { registeredEntityIds: defenderIds, deadEntityIds: defenderIds },
|
||||
}).snapshot;
|
||||
expect(snapshot.worldObjectStates[forcefield.id]).toBe("open");
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: altar.id, now: 30 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("sunken-temple:hakkar-pack-1");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 31);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 31 }).snapshot;
|
||||
expect(snapshot).toMatchObject({ phase: "idle", activeSpawnGroupIds: [] });
|
||||
expect(snapshot.worldObjectStates[altar.id]).toBe("ready");
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: altar.id, now: 40 }).snapshot;
|
||||
for (let index = 0; index < 4; index += 1) {
|
||||
const ritualGroup = group(definition, `sunken-temple:hakkar-pack-${index + 1}`);
|
||||
snapshot = killGroup(scripted, snapshot, ritualGroup, 41 + index * 2);
|
||||
expect(snapshot.worldObjectStates[flames[index].id]).toBe("ready");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "interact",
|
||||
objectId: flames[index].id,
|
||||
now: 42 + index * 2,
|
||||
}).snapshot;
|
||||
}
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("sunken-temple:avatar-of-hakkar");
|
||||
snapshot = killGroup(scripted, snapshot, group(definition, "sunken-temple:avatar-of-hakkar"), 60);
|
||||
const avatar = definition.bosses.find((objective) => objective.name === "Avatar of Hakkar")!;
|
||||
expect(snapshot.completedObjectiveIds).toContain(avatar.id);
|
||||
expect(flames.every((flame) => snapshot.worldObjectStates[flame.id] === "doused")).toBe(true);
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: "creature-39737", now: 70 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("sunken-temple:shade-of-eranikus");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 71);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 71 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("sunken-temple:shade-of-eranikus");
|
||||
snapshot = killGroup(scripted, snapshot, group(definition, "sunken-temple:shade-of-eranikus"), 72);
|
||||
const eranikus = definition.bosses.find((objective) => objective.name === "Shade of Eranikus")!;
|
||||
expect(snapshot.completedObjectiveIds).toContain(eranikus.id);
|
||||
expect(snapshot.checkpointId).toBe("sunken-temple-eranikus");
|
||||
});
|
||||
|
||||
it("runs BRD's deterministic arena, twelve-coffer vault, and exact seven-stage tomb with retry", () => {
|
||||
const definition = adapted("blackrock-depths");
|
||||
const scripted = encounter(definition);
|
||||
const arenaGate4 = objectBySpawn(definition, 15_309);
|
||||
const arenaGate3 = objectBySpawn(definition, 15_295);
|
||||
const arenaGroups = [
|
||||
group(definition, "blackrock-depths:ring-wave-1"),
|
||||
group(definition, "blackrock-depths:ring-wave-2"),
|
||||
group(definition, "blackrock-depths:ring-boss"),
|
||||
];
|
||||
expect(arenaGroups.map((spawnGroup) => spawnGroup.spawns.length)).toEqual([3, 3, 1]);
|
||||
let snapshot = reduceDungeonEncounter(scripted, createDungeonEncounterSnapshot(scripted), {
|
||||
kind: "interact",
|
||||
objectId: arenaGate4.id,
|
||||
now: 0,
|
||||
}).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 5_000 }).snapshot;
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 5_001);
|
||||
expect(snapshot.worldObjectStates[arenaGate4.id]).toBe("ready");
|
||||
expect(snapshot.activeSpawnGroupIds).toEqual([]);
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: arenaGate4.id, now: 6_000 }).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 11_000 }).snapshot;
|
||||
snapshot = killGroup(scripted, snapshot, arenaGroups[0], 11_001);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 16_001 }).snapshot;
|
||||
snapshot = killGroup(scripted, snapshot, arenaGroups[1], 16_002);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 21_002 }).snapshot;
|
||||
snapshot = killGroup(scripted, snapshot, arenaGroups[2], 21_003);
|
||||
expect(snapshot.completedObjectiveIds).toContain("blackrock-depths:ring-of-law-gorosh");
|
||||
expect(snapshot.worldObjectStates[arenaGate3.id]).toBe("open");
|
||||
expect(snapshot.worldObjectStates[arenaGate4.id]).toBe("open");
|
||||
|
||||
const coffers = [15_306, 15_329, 15_330, 15_331, 15_363, 15_364, 15_365, 15_536, 17_904, 35_801, 35_864, 39_924]
|
||||
.map((spawnId) => objectBySpawn(definition, spawnId));
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: coffers[index].id, now: 30 + index }).snapshot;
|
||||
}
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 40);
|
||||
expect(snapshot.counters["opened-coffers"]).toBe(6);
|
||||
expect(coffers.slice(0, 6).every((coffer) => snapshot.worldObjectStates[coffer.id] === "open")).toBe(true);
|
||||
for (let index = 6; index < coffers.length; index += 1) {
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: coffers[index].id, now: 41 + index }).snapshot;
|
||||
}
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 60 }).snapshot;
|
||||
const watchman = group(definition, "blackrock-depths:vault-watchman");
|
||||
expect(snapshot.activeSpawnGroupIds).toContain(watchman.id);
|
||||
snapshot = killGroup(scripted, snapshot, watchman, 61);
|
||||
expect(snapshot.completedObjectiveIds).toContain("blackrock-depths:watchman-doomgrip");
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 43_094).id]).toBe("open");
|
||||
|
||||
const marker = definition.serverGameObjects.find((object) => object.id === "blackrock-depths:doomrel-marker")!;
|
||||
const tombGroups = Array.from({ length: 7 }, (_, index) => group(definition, `blackrock-depths:tomb-spectral-${index + 1}`));
|
||||
expect(tombGroups.map((spawnGroup) => spawnGroup.spawns.length)).toEqual([1, 1, 1, 1, 1, 1, 1]);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: marker.id, now: 100 }).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 1_100 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain(tombGroups[0].id);
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 1_101);
|
||||
expect(snapshot).toMatchObject({ phase: "idle", counters: { "tomb-kills": 0 }, activeSpawnGroupIds: [] });
|
||||
expect(snapshot.worldObjectStates[marker.id]).toBe("ready");
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: marker.id, now: 2_000 }).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 3_000 }).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: tombGroups[0].spawns[0].id,
|
||||
now: 3_001,
|
||||
}).snapshot;
|
||||
for (let index = 1; index < tombGroups.length; index += 1) {
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "tick",
|
||||
now: snapshot.phaseStartedAt + 30_000,
|
||||
}).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: tombGroups[index].spawns[0].id,
|
||||
now: snapshot.phaseStartedAt + 1,
|
||||
}).snapshot;
|
||||
}
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: snapshot.phaseStartedAt + 2 }).snapshot;
|
||||
expect(snapshot.completedObjectiveIds).toContain("blackrock-depths:tomb-of-seven");
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 67_872).id]).toBe("available");
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 15_334).id]).toBe("open");
|
||||
});
|
||||
|
||||
it("gates Scholomance completion on six minibosses and a wipe-safe Gandling reveal", () => {
|
||||
const definition = adapted("scholomance");
|
||||
const scripted = encounter(definition);
|
||||
const brazier = objectBySpawn(definition, 43_208);
|
||||
const kirtonosGate = objectBySpawn(definition, 18_892);
|
||||
const gandlingEntrance = objectBySpawn(definition, 43_201);
|
||||
const progression = definition.encounters?.find((candidate) => candidate.id === "scholomance:boss-progression")!;
|
||||
expect(progression.rules.some((rule) => rule.id === "all-required-bosses-defeated")).toBe(false);
|
||||
|
||||
let snapshot = reduceDungeonEncounter(scripted, createDungeonEncounterSnapshot(scripted), {
|
||||
kind: "interact",
|
||||
objectId: brazier.id,
|
||||
now: 0,
|
||||
}).snapshot;
|
||||
expect(snapshot.worldObjectStates[kirtonosGate.id]).toBe("closed");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 1);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 1 }).snapshot;
|
||||
expect(snapshot.worldObjectStates[brazier.id]).toBe("ready");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "interact", objectId: brazier.id, now: 2 }).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 5_002 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("scholomance:kirtonos");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: "scholomance:kirtonos-the-herald",
|
||||
now: 5_003,
|
||||
}).snapshot;
|
||||
expect(snapshot.completedObjectiveIds).toContain("scholomance:kirtonos-the-herald");
|
||||
expect(snapshot.worldObjectStates[kirtonosGate.id]).toBe("open");
|
||||
|
||||
const miniBossIds = ["creature-48867", "creature-48925", "creature-48946", "creature-48863", "creature-48857", "creature-48854"];
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "tick",
|
||||
now: 6_000,
|
||||
facts: { registeredEntityIds: miniBossIds, deadEntityIds: miniBossIds },
|
||||
}).snapshot;
|
||||
expect(snapshot.phase).toBe("gandling-reveal");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 6_001);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 6_001 }).snapshot;
|
||||
expect(snapshot).toMatchObject({ phase: "gandling-ready", flags: { "gandling-unlocked": true } });
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("scholomance:gandling");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-engaged",
|
||||
entityId: "creature-247104",
|
||||
now: 6_002,
|
||||
}).snapshot;
|
||||
expect(snapshot.worldObjectStates[gandlingEntrance.id]).toBe("closed");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 6_003);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 6_003 }).snapshot;
|
||||
expect(snapshot.worldObjectStates[gandlingEntrance.id]).toBe("open");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: "creature-247104",
|
||||
now: 6_004,
|
||||
}).snapshot;
|
||||
expect(snapshot.completedObjectiveIds).toContain("creature-247104");
|
||||
|
||||
const fleetRequired = definition.bosses.filter((objective) => !objective.optional && !objective.eventOnly);
|
||||
for (const objective of fleetRequired) {
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: objective.id,
|
||||
now: snapshot.revision + 10_000,
|
||||
}).snapshot;
|
||||
}
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 20_000 }).snapshot;
|
||||
expect(snapshot).toMatchObject({ phase: "complete", flags: { "dungeon-complete": true } });
|
||||
});
|
||||
|
||||
it("persists and resumes Stratholme's exact 3-to-13-to-1-to-33-to-5 slaughter chain", () => {
|
||||
const definition = adapted("stratholme");
|
||||
const scripted = encounter(definition);
|
||||
const acolytes = Array.from({ length: 3 }, (_, index) => group(definition, `stratholme:ziggurat-${index + 1}-acolyte`));
|
||||
const zigguratBosses = ["creature-54238", "creature-54239", "creature-54240"];
|
||||
const zigguratDoors = [11_415, 11_414, 11_416].map((spawnId) => objectBySpawn(definition, spawnId));
|
||||
const mindless = group(definition, "stratholme:mindless-undead");
|
||||
const guards = group(definition, "stratholme:black-guards");
|
||||
expect(acolytes.map((spawnGroup) => spawnGroup.spawns.length)).toEqual([1, 1, 1]);
|
||||
expect(mindless.spawns).toHaveLength(33);
|
||||
expect(guards.spawns).toHaveLength(5);
|
||||
let snapshot = createDungeonEncounterSnapshot(scripted);
|
||||
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "entity-death", entityId: zigguratBosses[0], now: 1 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain(acolytes[0].id);
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 2);
|
||||
expect(snapshot.worldObjectStates[zigguratDoors[0].id]).toBe("open");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 2 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain(acolytes[0].id);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: acolytes[0].spawns[0].id,
|
||||
now: 3,
|
||||
}).snapshot;
|
||||
for (let index = 1; index < 3; index += 1) {
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: zigguratBosses[index],
|
||||
now: 4 + index * 2,
|
||||
}).snapshot;
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: acolytes[index].spawns[0].id,
|
||||
now: 5 + index * 2,
|
||||
}).snapshot;
|
||||
}
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 20 }).snapshot;
|
||||
expect(snapshot.flags["ziggurats-complete"]).toBe(true);
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 6_911).id]).toBe("open");
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 6_908).id]).toBe("open");
|
||||
|
||||
const slaughterIds = [
|
||||
"creature-53969", "creature-54002", "creature-54018", "creature-54019",
|
||||
"creature-54020", "creature-54021", "creature-54022", "creature-54026", "creature-54027",
|
||||
"creature-54039", "creature-54040", "creature-54041", "creature-54050",
|
||||
];
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "tick",
|
||||
now: 21,
|
||||
facts: { registeredEntityIds: slaughterIds, deadEntityIds: slaughterIds },
|
||||
}).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("stratholme:ramstein");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 22);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 22 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("stratholme:ramstein");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: "stratholme:ramstein-the-gorger",
|
||||
now: 23,
|
||||
}).snapshot;
|
||||
expect(snapshot.phase).toBe("mindless-delay");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "checkpoint", 24);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 24 }).snapshot;
|
||||
expect(snapshot.phase).toBe("mindless-delay");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 5_024 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain(mindless.id);
|
||||
snapshot = killGroup(scripted, snapshot, mindless, 5_025);
|
||||
expect(snapshot.phase).toBe("black-guard-delay");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 5_026);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 5_026 }).snapshot;
|
||||
expect(snapshot.phase).toBe("black-guard-delay");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 25_026 }).snapshot;
|
||||
expect(snapshot.activeSpawnGroupIds).toContain(guards.id);
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 11_808).id]).toBe("open");
|
||||
snapshot = killGroup(scripted, snapshot, guards, 25_027);
|
||||
expect(snapshot.activeSpawnGroupIds).toContain("stratholme:baron-rivendare");
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 35_848).id]).toBe("open");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-engaged",
|
||||
entityId: "creature-54241",
|
||||
now: 25_028,
|
||||
}).snapshot;
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 35_848).id]).toBe("closed");
|
||||
snapshot = resetDungeonEncounterSnapshot(scripted, snapshot, "boss-wipe", 25_029);
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, { kind: "tick", now: 25_029 }).snapshot;
|
||||
expect(snapshot).toMatchObject({ phase: "baron-ready", flags: { "guards-defeated": true } });
|
||||
expect(snapshot.worldObjectStates[objectBySpawn(definition, 35_848).id]).toBe("open");
|
||||
snapshot = reduceDungeonEncounter(scripted, snapshot, {
|
||||
kind: "entity-death",
|
||||
entityId: "creature-54241",
|
||||
now: 25_030,
|
||||
}).snapshot;
|
||||
expect(snapshot.completedObjectiveIds).toContain("creature-54241");
|
||||
expect(snapshot.checkpointId).toBe("creature-54241");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user