forked from phenom/healer-man
244 lines
12 KiB
TypeScript
244 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { DUNGEON_REGISTRY } from "./dungeonRegistry";
|
|
import {
|
|
createDungeonEncounterSnapshot,
|
|
reduceDungeonEncounter,
|
|
resetDungeonEncounterSnapshot,
|
|
validateDungeonEncounterDefinition,
|
|
} from "./dungeonEncounterRuntime";
|
|
import { WOW_PROGRESSION_OBJECT_REVIEWS } from "./wowProgressionObjectContentCatalog";
|
|
|
|
function encounter(dungeonId: string, suffix: string) {
|
|
const definition = DUNGEON_REGISTRY[dungeonId];
|
|
const result = definition.encounters?.find((candidate) => candidate.id === `${dungeonId}:${suffix}`);
|
|
if (!result) throw new Error(`Missing ${dungeonId}:${suffix}`);
|
|
return result;
|
|
}
|
|
|
|
function actorId(dungeonId: string, entry: number): string {
|
|
const definition = DUNGEON_REGISTRY[dungeonId];
|
|
const staticSpawn = definition.staticSpawns.find(
|
|
(spawn) => definition.entities[spawn.entityId]?.combat?.serverEntry === entry,
|
|
);
|
|
if (staticSpawn) return staticSpawn.id;
|
|
for (const pack of definition.roamingPacks) {
|
|
const member = pack.members.find(
|
|
(candidate) => definition.entities[candidate.entityId]?.combat?.serverEntry === entry,
|
|
);
|
|
if (member) return `${pack.id}:${member.id}`;
|
|
}
|
|
throw new Error(`${dungeonId} is missing entry ${entry}`);
|
|
}
|
|
|
|
describe("source-backed progression objects", () => {
|
|
it("pins source hashes and keeps every adaptation and remaining gap explicit", () => {
|
|
expect(WOW_PROGRESSION_OBJECT_REVIEWS.map((review) => review.dungeonId)).toEqual([
|
|
"deadmines",
|
|
"shadowfang-keep",
|
|
"maraudon",
|
|
"mechanar",
|
|
"steamvault",
|
|
"shadow-labyrinth",
|
|
"scarlet-monastery",
|
|
]);
|
|
for (const review of WOW_PROGRESSION_OBJECT_REVIEWS) {
|
|
expect(review.files.length).toBeGreaterThan(0);
|
|
expect(review.files.every((file) => /sha256 [a-f0-9]{64}/.test(file))).toBe(true);
|
|
expect(review.adaptations.length).toBeGreaterThan(0);
|
|
expect(review.remainingGaps.length).toBeGreaterThan(0);
|
|
const runtime = DUNGEON_REGISTRY[review.dungeonId].encounters?.find(
|
|
(candidate) => candidate.source?.reviewedBehavior === review.reviewedBehavior,
|
|
);
|
|
expect(runtime).toBeDefined();
|
|
validateDungeonEncounterDefinition(runtime!);
|
|
}
|
|
});
|
|
|
|
it("runs Deadmines factory, foundry, and cannon breach transitions", () => {
|
|
const dungeon = DUNGEON_REGISTRY.deadmines;
|
|
const definition = encounter("deadmines", "progression-objects");
|
|
const factory = dungeon.serverGameObjects.find((object) => object.source?.templateId === 13_965)!;
|
|
const foundry = dungeon.serverGameObjects.find((object) => object.source?.templateId === 16_399)!;
|
|
const ironClad = dungeon.serverGameObjects.find((object) => object.source?.templateId === 16_397)!;
|
|
const foundryLever = dungeon.serverGameObjects.find((object) => object.source?.templateId === 101_834)!;
|
|
const cannon = dungeon.serverGameObjects.find((object) => object.source?.templateId === 16_398)!;
|
|
let snapshot = createDungeonEncounterSnapshot(definition);
|
|
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("deadmines", 644),
|
|
now: 1,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[factory.id]).toBe("open");
|
|
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "interact",
|
|
objectId: foundryLever.id,
|
|
now: 2,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[foundry.id]).toBe("open");
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "interact",
|
|
objectId: cannon.id,
|
|
now: 3,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[ironClad.id]).toBe("open");
|
|
expect(snapshot.worldObjectStates[cannon.id]).toBe("used");
|
|
expect(snapshot.activeSpawnGroupIds).toContain("deadmines:cannon-reinforcements");
|
|
expect(dungeon.encounterSpawnGroups?.find((group) => group.id === "deadmines:cannon-reinforcements")?.spawns).toHaveLength(2);
|
|
|
|
const reset = resetDungeonEncounterSnapshot(definition, snapshot, "boss-wipe", 4);
|
|
expect(reset.worldObjectStates[factory.id]).toBe("open");
|
|
expect(reset.worldObjectStates[foundry.id]).toBe("open");
|
|
expect(reset.worldObjectStates[ironClad.id]).toBe("open");
|
|
expect(reset.activeSpawnGroupIds).not.toContain("deadmines:cannon-reinforcements");
|
|
});
|
|
|
|
it("opens each Shadowfang Keep progression door at its source milestone", () => {
|
|
const dungeon = DUNGEON_REGISTRY["shadowfang-keep"];
|
|
const definition = encounter("shadowfang-keep", "progression-objects");
|
|
const courtyard = dungeon.serverGameObjects.find((object) => object.source?.templateId === 18_895)!;
|
|
const sorcerer = dungeon.serverGameObjects.find((object) => object.source?.templateId === 18_972)!;
|
|
const arugal = dungeon.serverGameObjects.find((object) => object.source?.templateId === 18_971)!;
|
|
let snapshot = createDungeonEncounterSnapshot(definition);
|
|
|
|
for (const [entry, objectId, now] of [
|
|
[3_914, courtyard.id, 1],
|
|
[4_274, sorcerer.id, 2],
|
|
[3_927, arugal.id, 3],
|
|
] as const) {
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("shadowfang-keep", entry),
|
|
now,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[objectId]).toBe("open");
|
|
}
|
|
|
|
const reset = resetDungeonEncounterSnapshot(definition, snapshot, "checkpoint", 4);
|
|
expect(reset.worldObjectStates[courtyard.id]).toBe("open");
|
|
expect(reset.worldObjectStates[sorcerer.id]).toBe("open");
|
|
expect(reset.worldObjectStates[arugal.id]).toBe("open");
|
|
});
|
|
|
|
it("opens Maraudon's Corruption Spewer when Noxxion dies", () => {
|
|
const dungeon = DUNGEON_REGISTRY.maraudon;
|
|
const definition = encounter("maraudon", "progression-objects");
|
|
const spewer = dungeon.serverGameObjects.find((object) => object.source?.templateId === 178_570)!;
|
|
let snapshot = createDungeonEncounterSnapshot(definition);
|
|
expect(snapshot.worldObjectStates[spewer.id]).toBe("closed");
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("maraudon", 13_282),
|
|
now: 1,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[spewer.id]).toBe("open");
|
|
expect(resetDungeonEncounterSnapshot(definition, snapshot, "boss-wipe", 2).worldObjectStates[spewer.id]).toBe("open");
|
|
});
|
|
|
|
it("opens Mechanar passage doors and resets Sepethrea's room door safely", () => {
|
|
const dungeon = DUNGEON_REGISTRY.mechanar;
|
|
const definition = encounter("mechanar", "progression-objects");
|
|
let snapshot = createDungeonEncounterSnapshot(definition);
|
|
const ironDoor = dungeon.serverGameObjects.find((object) => object.source?.templateId === 184_632)!;
|
|
const gyroDoor = dungeon.serverGameObjects.find((object) => object.source?.templateId === 184_322)!;
|
|
const sepethreaDoor = dungeon.serverGameObjects.find((object) => object.source?.templateId === 184_449)!;
|
|
|
|
expect(ironDoor.states.find((state) => state.id === "closed")?.blocksMovement).toBe(true);
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("mechanar", 19_710),
|
|
now: 1,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[ironDoor.id]).toBe("open");
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("mechanar", 19_218),
|
|
now: 2,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[gyroDoor.id]).toBe("open");
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-engaged",
|
|
entityId: actorId("mechanar", 19_221),
|
|
now: 3,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[sepethreaDoor.id]).toBe("closed");
|
|
snapshot = resetDungeonEncounterSnapshot(definition, snapshot, "boss-wipe", 4);
|
|
expect(snapshot.worldObjectStates[ironDoor.id]).toBe("open");
|
|
expect(snapshot.worldObjectStates[gyroDoor.id]).toBe("open");
|
|
expect(snapshot.worldObjectStates[sepethreaDoor.id]).toBe("open");
|
|
});
|
|
|
|
it("requires both Steamvault boss panels before opening the main chamber", () => {
|
|
const dungeon = DUNGEON_REGISTRY.steamvault;
|
|
const definition = encounter("steamvault", "access-panels");
|
|
let snapshot = createDungeonEncounterSnapshot(definition);
|
|
const hydro = dungeon.serverGameObjects.find((object) => object.source?.templateId === 184_125)!;
|
|
const mek = dungeon.serverGameObjects.find((object) => object.source?.templateId === 184_126)!;
|
|
const door = dungeon.serverGameObjects.find((object) => object.source?.templateId === 183_049)!;
|
|
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("steamvault", 17_797),
|
|
now: 1,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[hydro.id]).toBe("unlocked");
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, { kind: "interact", objectId: hydro.id, now: 2 }).snapshot;
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, { kind: "tick", now: 3 }).snapshot;
|
|
expect(snapshot.worldObjectStates[door.id]).toBe("closed");
|
|
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, {
|
|
kind: "entity-death",
|
|
entityId: actorId("steamvault", 17_796),
|
|
now: 4,
|
|
}).snapshot;
|
|
expect(snapshot.worldObjectStates[mek.id]).toBe("unlocked");
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, { kind: "interact", objectId: mek.id, now: 5 }).snapshot;
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, { kind: "tick", now: 6 }).snapshot;
|
|
expect(snapshot.worldObjectStates[door.id]).toBe("open");
|
|
const reset = resetDungeonEncounterSnapshot(definition, snapshot, "boss-wipe", 7);
|
|
expect(reset.worldObjectStates[door.id]).toBe("open");
|
|
expect(reset.worldObjectStates[hydro.id]).toBe("used");
|
|
expect(reset.worldObjectStates[mek.id]).toBe("used");
|
|
});
|
|
|
|
it("holds Hellmaw out of static population until all common ritualists die", () => {
|
|
const dungeon = DUNGEON_REGISTRY["shadow-labyrinth"];
|
|
const definition = encounter("shadow-labyrinth", "progression-objects");
|
|
const ritualists = dungeon.staticSpawns
|
|
.filter((spawn) => dungeon.entities[spawn.entityId]?.combat?.serverEntry === 18_794 && (spawn.spawnMask ?? 3) === 3)
|
|
.map((spawn) => spawn.id);
|
|
const hellmawGroup = dungeon.encounterSpawnGroups?.find(
|
|
(group) => group.id === "shadow-labyrinth:hellmaw",
|
|
);
|
|
expect(ritualists).toHaveLength(18);
|
|
expect(dungeon.staticSpawns.some((spawn) => dungeon.entities[spawn.entityId]?.combat?.serverEntry === 18_731)).toBe(false);
|
|
expect(hellmawGroup?.spawns).toHaveLength(1);
|
|
const result = reduceDungeonEncounter(definition, createDungeonEncounterSnapshot(definition), {
|
|
kind: "tick",
|
|
now: 1,
|
|
facts: { registeredEntityIds: ritualists, deadEntityIds: ritualists },
|
|
});
|
|
expect(result.snapshot.activeSpawnGroupIds).toContain("shadow-labyrinth:hellmaw");
|
|
});
|
|
|
|
it("gates Whitemane behind Mograine and restores her attempt after a wipe", () => {
|
|
const dungeon = DUNGEON_REGISTRY["scarlet-monastery"];
|
|
const definition = encounter("scarlet-monastery", "cathedral-pair");
|
|
const groupId = "scarlet-monastery:whitemane";
|
|
expect(dungeon.staticSpawns.some((spawn) => dungeon.entities[spawn.entityId]?.combat?.serverEntry === 3_977)).toBe(false);
|
|
expect(dungeon.encounterSpawnGroups?.find((group) => group.id === groupId)?.spawns).toHaveLength(1);
|
|
|
|
let snapshot = reduceDungeonEncounter(definition, createDungeonEncounterSnapshot(definition), {
|
|
kind: "entity-death",
|
|
entityId: actorId("scarlet-monastery", 3_976),
|
|
now: 1,
|
|
}).snapshot;
|
|
expect(snapshot.activeSpawnGroupIds).toContain(groupId);
|
|
snapshot = resetDungeonEncounterSnapshot(definition, snapshot, "boss-wipe", 2);
|
|
expect(snapshot.activeSpawnGroupIds).not.toContain(groupId);
|
|
snapshot = reduceDungeonEncounter(definition, snapshot, { kind: "tick", now: 3 }).snapshot;
|
|
expect(snapshot.activeSpawnGroupIds).toContain(groupId);
|
|
});
|
|
});
|