167 lines
7.3 KiB
TypeScript
167 lines
7.3 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { WAILING_CAVERNS_DEFINITION } from "./dungeons/wailingCaverns";
|
|
import referenceData from "./generated/azerothCoreReference.json";
|
|
import { WAILING_CAVERNS_STATIC_SPAWNS } from "./mobPopulation";
|
|
import {
|
|
useWailingEncounterStore,
|
|
wailingEventSpawns,
|
|
wailingStaticSpawnsForPhase,
|
|
} from "./wailingCavernsEncounter";
|
|
|
|
interface WailingReferenceShape {
|
|
readonly creature: {
|
|
readonly templates: Readonly<Record<string, {
|
|
readonly mechanicImmunities: readonly string[];
|
|
}>>;
|
|
};
|
|
readonly wailingCaverns: {
|
|
readonly requiredBossEntries: readonly number[];
|
|
readonly despawnOnInitializeEntries: readonly number[];
|
|
readonly unsupportedRequiredBossActions: readonly unknown[];
|
|
readonly abilitiesByEntry: Readonly<Record<string, readonly {
|
|
readonly id: string;
|
|
readonly name: string;
|
|
readonly target: string;
|
|
readonly initialCooldownMs: readonly [number, number];
|
|
readonly repeatCooldownMs: readonly [number, number];
|
|
readonly maximumCasts?: number;
|
|
readonly condition: Readonly<Record<string, unknown>>;
|
|
readonly effect: Readonly<Record<string, unknown>>;
|
|
}[]>>;
|
|
};
|
|
}
|
|
|
|
const REFERENCE = referenceData as unknown as WailingReferenceShape;
|
|
const deadMob = { dead: true } as never;
|
|
|
|
describe("AzerothCore-backed Wailing Caverns parity slice", () => {
|
|
afterEach(() => useWailingEncounterStore.getState().reset());
|
|
|
|
it("uses exactly the server's eight required bosses and classifies Serpentis as a boss", () => {
|
|
expect(WAILING_CAVERNS_DEFINITION.objectiveOrder.map((boss) => boss.name)).toEqual([
|
|
"Lady Anacondra",
|
|
"Kresh",
|
|
"Lord Cobrahn",
|
|
"Lord Pythas",
|
|
"Skum",
|
|
"Lord Serpentis",
|
|
"Verdan the Everliving",
|
|
"Mutanus the Devourer",
|
|
]);
|
|
expect(REFERENCE.wailingCaverns.requiredBossEntries).toEqual([
|
|
3671, 3669, 3653, 3670, 3674, 3673, 5775, 3654,
|
|
]);
|
|
expect(WAILING_CAVERNS_DEFINITION.entities["lord-serpentis"].kind).toBe("boss");
|
|
expect(REFERENCE.wailingCaverns.despawnOnInitializeEntries).toEqual([5912]);
|
|
expect(REFERENCE.wailingCaverns.unsupportedRequiredBossActions).toEqual([]);
|
|
});
|
|
|
|
it("imports exact Fanglord targets, health gates, one-shot forms, and cooldown ranges", () => {
|
|
const cobrahn = REFERENCE.wailingCaverns.abilitiesByEntry["3669"];
|
|
expect(cobrahn.find((ability) => ability.id === "spell-9532")).toMatchObject({
|
|
target: "primary",
|
|
initialCooldownMs: [0, 0],
|
|
repeatCooldownMs: [2_400, 3_800],
|
|
effect: { kind: "damage" },
|
|
});
|
|
expect(cobrahn.find((ability) => ability.id === "spell-8040")).toMatchObject({
|
|
target: "random-party",
|
|
initialCooldownMs: [8_000, 11_000],
|
|
repeatCooldownMs: [10_000, 20_000],
|
|
effect: { kind: "control", mechanic: "sleep" },
|
|
});
|
|
expect(cobrahn.find((ability) => ability.id === "spell-5187")).toMatchObject({
|
|
target: "lowest-health-friendly",
|
|
initialCooldownMs: [0, 0],
|
|
repeatCooldownMs: [12_000, 18_000],
|
|
condition: { kind: "friendly-missing-health", amount: 400, range: 40 },
|
|
effect: { kind: "heal" },
|
|
});
|
|
expect(cobrahn.find((ability) => ability.id === "spell-7965")).toMatchObject({
|
|
target: "self",
|
|
maximumCasts: 1,
|
|
condition: { kind: "self-health-percent", minimum: 0, maximum: 30 },
|
|
effect: { kind: "transform", form: "serpent" },
|
|
});
|
|
});
|
|
|
|
it("uses imported immunities instead of treating every boss as fear-immune", () => {
|
|
expect(REFERENCE.creature.templates["3653"].mechanicImmunities).not.toContain("fear");
|
|
expect(REFERENCE.creature.templates["3671"].mechanicImmunities).not.toContain("fear");
|
|
expect(REFERENCE.creature.templates["3671"].mechanicImmunities).toContain("sleep");
|
|
});
|
|
|
|
it("never gives a server-backed Wailing creature a synthetic Surge", () => {
|
|
const attacks = Object.values(WAILING_CAVERNS_DEFINITION.entities)
|
|
.flatMap((entity) => entity.combat?.attacks ?? []);
|
|
expect(attacks.some((attack) => /surge/i.test(attack.name))).toBe(false);
|
|
});
|
|
|
|
it("keeps Faerie Dragon despawned and makes Mutanus event-only", () => {
|
|
const locked = wailingStaticSpawnsForPhase(WAILING_CAVERNS_STATIC_SPAWNS, "locked");
|
|
expect(locked.some((spawn) => spawn.id === "faerie-dragon-spawn")).toBe(false);
|
|
expect(locked.some((spawn) => spawn.id === "mutanus-spawn")).toBe(false);
|
|
const mutanus = wailingStaticSpawnsForPhase(WAILING_CAVERNS_STATIC_SPAWNS, "mutanus");
|
|
expect(mutanus.some((spawn) => spawn.id === "mutanus-spawn")).toBe(true);
|
|
});
|
|
|
|
it("gates Naralex behind the Fanglords, summons exact waves, and recovers from soft locks", () => {
|
|
const encounter = useWailingEncounterStore.getState();
|
|
encounter.advance({
|
|
"lady-anacondra-spawn": deadMob,
|
|
"lord-cobrahn-spawn": deadMob,
|
|
"lord-pythas-spawn": deadMob,
|
|
"lord-serpentis-spawn": deadMob,
|
|
}, 1_000);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("ready");
|
|
expect(useWailingEncounterStore.getState().start(2_000)).toBe(true);
|
|
|
|
useWailingEncounterStore.getState().advance({}, 10_000);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("ravager-wave");
|
|
expect(wailingEventSpawns("ravager-wave")).toHaveLength(2);
|
|
|
|
useWailingEncounterStore.getState().advance({}, 70_001);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("escort-serpents");
|
|
useWailingEncounterStore.getState().advance({}, 82_001);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("serpent-wave");
|
|
expect(wailingEventSpawns("serpent-wave")).toHaveLength(3);
|
|
|
|
useWailingEncounterStore.getState().advance({}, 142_002);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("cleansing");
|
|
useWailingEncounterStore.getState().advance({}, 174_502);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("nightmare-wave");
|
|
expect(wailingEventSpawns("nightmare-wave")).toHaveLength(6);
|
|
|
|
useWailingEncounterStore.getState().advance({}, 264_503);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("mutanus");
|
|
useWailingEncounterStore.getState().advance({ "mutanus-spawn": deadMob }, 264_504);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("complete");
|
|
expect(useWailingEncounterStore.getState().flags["dungeon-complete"]).toBe(true);
|
|
expect(useWailingEncounterStore.getState().checkpointId).toBe("mutanus-spawn");
|
|
});
|
|
|
|
it("restores the Naralex unlock after a checkpoint reload suppresses the cleared Fanglords", () => {
|
|
useWailingEncounterStore.getState().advance({
|
|
"lady-anacondra-spawn": deadMob,
|
|
"lord-cobrahn-spawn": deadMob,
|
|
"lord-pythas-spawn": deadMob,
|
|
"lord-serpentis-spawn": deadMob,
|
|
}, 1_000);
|
|
expect(useWailingEncounterStore.getState()).toMatchObject({
|
|
phase: "ready",
|
|
flags: { "fanglords-complete": true },
|
|
});
|
|
|
|
useWailingEncounterStore.getState().reset("checkpoint", 2_000);
|
|
expect(useWailingEncounterStore.getState()).toMatchObject({
|
|
phase: "locked",
|
|
flags: { "fanglords-complete": true },
|
|
});
|
|
|
|
// A loaded checkpoint intentionally has no live Fanglord runtime ids: the
|
|
// progression provider has already suppressed those completed bosses.
|
|
useWailingEncounterStore.getState().advance({}, 2_100);
|
|
expect(useWailingEncounterStore.getState().phase).toBe("ready");
|
|
});
|
|
});
|