198 lines
8.4 KiB
TypeScript
198 lines
8.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { buildBossGuideCatalog, buildDungeonBossGuide, bossGuideOverrideKey } from "./bossGuide";
|
||
import { DUNGEON_DEFINITION_SCHEMA_VERSION, type DungeonDefinition } from "./dungeonTypes";
|
||
import { DUNGEON_DEFINITIONS } from "./dungeonRegistry";
|
||
import { BOSS_GUIDE_OVERRIDES } from "./bossGuideOverrides";
|
||
|
||
const fixture = {
|
||
schemaVersion: DUNGEON_DEFINITION_SCHEMA_VERSION,
|
||
id: "guide-fixture",
|
||
title: "Guide Fixture",
|
||
mapId: 1,
|
||
lfgDungeonIds: [],
|
||
expansion: "runewaker",
|
||
environmentMode: "runewaker-ros",
|
||
difficultyVariants: [{ id: "normal", difficulty: "normal", spawnMask: 1, levelRange: [1, 2] }],
|
||
defaultDifficultyId: "normal",
|
||
availability: "available",
|
||
presentation: {
|
||
location: "Test",
|
||
theme: "Test",
|
||
summary: "Test",
|
||
loadingMessage: "Test",
|
||
unchartedAreaName: "Test",
|
||
background: "#000",
|
||
fog: { color: "#000", near: 1, far: 2 },
|
||
ambientLight: { color: "#fff", intensity: 1 },
|
||
hemisphereLight: { skyColor: "#fff", groundColor: "#000", intensity: 1 },
|
||
directionalLight: { color: "#fff", intensity: 1, offset: [0, 1, 0] },
|
||
materials: { cutoutPatterns: [], cutoutNames: [], blendPatterns: [], blendNames: [], additivePatterns: [], additiveNames: [] },
|
||
map: { width: 100, height: 100, padding: 1, contours: [] },
|
||
},
|
||
assets: { visual: [], collision: [] },
|
||
transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: 1 },
|
||
bounds: { min: [0, 0, 0], max: [1, 1, 1] },
|
||
entrance: { name: "Entrance", footPosition: [0, 0, 0], forward: [0, 0, 1], yaw: 0 },
|
||
areas: [],
|
||
entities: {
|
||
herald: {
|
||
id: "herald",
|
||
name: "The Herald",
|
||
kind: "boss",
|
||
title: "Keeper of Tests",
|
||
combat: {
|
||
attacks: [
|
||
{
|
||
id: "basic-attack",
|
||
name: "Basic Attack",
|
||
delivery: "melee",
|
||
target: "primary",
|
||
school: "physical",
|
||
animation: "attack",
|
||
range: 4,
|
||
cooldownMs: 2_000,
|
||
damageMultiplier: 1,
|
||
},
|
||
{
|
||
id: "falling-test",
|
||
name: "Falling Test",
|
||
spellId: 42,
|
||
delivery: "area",
|
||
target: "random-party",
|
||
school: "arcane",
|
||
animation: "cast",
|
||
castTimeMs: 2_000,
|
||
range: 30,
|
||
cooldownMs: 12_000,
|
||
initialCooldownMs: [8_000, 10_000],
|
||
repeatCooldownMs: [12_000, 14_000],
|
||
maximumCasts: 1,
|
||
damageMultiplier: 1.2,
|
||
telegraph: { shape: "circle", anchor: "target", radius: 6, warningMs: 2_000 },
|
||
condition: { kind: "self-health-percent", minimum: 0, maximum: 50 },
|
||
},
|
||
{
|
||
id: "source-gap-sweep",
|
||
name: "Fallback Sweep",
|
||
delivery: "area",
|
||
target: "nearby-party",
|
||
school: "physical",
|
||
animation: "attack",
|
||
castTimeMs: 1_800,
|
||
range: 8,
|
||
cooldownMs: 10_000,
|
||
damageMultiplier: 1,
|
||
telegraph: { shape: "cone", anchor: "source", length: 8, angleDegrees: 90, warningMs: 1_800 },
|
||
},
|
||
],
|
||
},
|
||
visual: { primaryColor: "#000", accentColor: "#fff", scale: 1, archetype: "humanoid" },
|
||
},
|
||
sentinel: {
|
||
id: "sentinel",
|
||
name: "Silent Sentinel",
|
||
kind: "boss",
|
||
title: "The Unscripted",
|
||
combat: { attacks: [] },
|
||
visual: { primaryColor: "#000", accentColor: "#fff", scale: 1, archetype: "humanoid" },
|
||
},
|
||
},
|
||
staticSpawns: [
|
||
{ id: "herald-spawn", entityId: "herald", position: [0, 0, 0] },
|
||
{ id: "sentinel-spawn", entityId: "sentinel", position: [1, 0, 0] },
|
||
],
|
||
roamingPacks: [],
|
||
bosses: [
|
||
{ id: "herald-spawn", name: "The Herald", position: [0, 0, 0] },
|
||
{ id: "sentinel-spawn", name: "Silent Sentinel", position: [1, 0, 0] },
|
||
],
|
||
objectiveOrder: [],
|
||
navigationLinks: [],
|
||
serverGameObjects: [],
|
||
provenance: { clientBuild: "test", clientArchiveHashes: {}, databaseAdapter: "test", coordinateTransform: "test" },
|
||
validation: { status: "green", blockers: [], warnings: [] },
|
||
} as const satisfies DungeonDefinition;
|
||
|
||
describe("boss guide model", () => {
|
||
it("derives health triggers, timing, telegraph geometry, instructions, and fidelity from runtime attacks", () => {
|
||
const guide = buildDungeonBossGuide(fixture, {
|
||
[bossGuideOverrideKey("guide-fixture", "herald", "falling-test")]: {
|
||
counterplay: "Use the authored safe spot.",
|
||
},
|
||
});
|
||
const mechanic = guide.bosses[0].mechanics[0];
|
||
|
||
expect(mechanic).toMatchObject({
|
||
id: "falling-test",
|
||
trigger: "When the boss reaches 50% health",
|
||
timing: "First: 8s–10s · Repeats: 12s–14s · Cast: 2s · Maximum uses: 1",
|
||
targetAndEffect: "Arcane damage to a random party member (1.2× boss power)",
|
||
telegraph: "6m radius circle centered on the target · 2s warning",
|
||
counterplay: "Use the authored safe spot.",
|
||
fidelity: "source",
|
||
fidelityLabel: "Source backed",
|
||
});
|
||
expect(mechanic.testInstruction).toContain("cross the threshold");
|
||
expect(guide.bosses[0].mechanics[1]).toMatchObject({
|
||
fidelity: "fallback",
|
||
fidelityLabel: "HealerMan fallback",
|
||
});
|
||
});
|
||
|
||
it("omits basic attacks and preserves an honest empty entry for a boss with no specials", () => {
|
||
const guide = buildDungeonBossGuide(fixture);
|
||
expect(guide.bosses[0].mechanics.map((mechanic) => mechanic.id)).toEqual([
|
||
"falling-test",
|
||
"source-gap-sweep",
|
||
]);
|
||
expect(guide.bosses[1].mechanics).toEqual([]);
|
||
});
|
||
|
||
it("covers every registered boss objective and every non-basic runtime mechanic", () => {
|
||
const catalog = buildBossGuideCatalog(DUNGEON_DEFINITIONS, BOSS_GUIDE_OVERRIDES);
|
||
const guideByDungeon = new Map(catalog.map((guide) => [guide.id, guide]));
|
||
|
||
for (const dungeon of DUNGEON_DEFINITIONS) {
|
||
const guide = guideByDungeon.get(dungeon.id);
|
||
expect(guide, dungeon.id).toBeDefined();
|
||
expect(guide!.bosses.length, dungeon.id).toBeGreaterThanOrEqual(dungeon.bosses.length);
|
||
for (const objective of dungeon.bosses) {
|
||
const boss = guide!.bosses.find((entry) => entry.id === objective.id);
|
||
expect(boss, `${dungeon.id}:${objective.name}`).toBeDefined();
|
||
expect(boss!.entityId, `${dungeon.id}:${objective.name}`).toBeDefined();
|
||
}
|
||
for (const boss of guide!.bosses) {
|
||
if (!boss.entityId) continue;
|
||
const expectedIds = dungeon.entities[boss.entityId]?.combat?.attacks
|
||
.filter((attack) => attack.id !== "basic-attack")
|
||
.map((attack) => attack.id) ?? [];
|
||
expect(boss.mechanics.map((mechanic) => mechanic.id), `${dungeon.id}:${boss.name}`).toEqual(expectedIds);
|
||
for (const mechanic of boss.mechanics) {
|
||
expect(mechanic.trigger, `${dungeon.id}:${boss.name}:${mechanic.name}:trigger`).not.toBe("");
|
||
expect(mechanic.timing, `${dungeon.id}:${boss.name}:${mechanic.name}:timing`).not.toBe("");
|
||
expect(mechanic.telegraph, `${dungeon.id}:${boss.name}:${mechanic.name}:telegraph`).not.toBe("");
|
||
expect(mechanic.counterplay, `${dungeon.id}:${boss.name}:${mechanic.name}:counterplay`).not.toBe("");
|
||
expect(mechanic.testInstruction, `${dungeon.id}:${boss.name}:${mechanic.name}:test`).not.toBe("");
|
||
}
|
||
}
|
||
}
|
||
|
||
const archaedas = guideByDungeon.get("uldaman")!.bosses.find((boss) => boss.name === "Archaedas")!;
|
||
expect(archaedas.mechanics.find((mechanic) => mechanic.id === "archaedas-awaken-earthen-guardians"))
|
||
.toMatchObject({
|
||
fidelityLabel: "Source backed",
|
||
counterplay: "At 70% health, the tank gathers the six awakened guardians while the party clears them together.",
|
||
});
|
||
const flowingChaos = guideByDungeon.get("forsaken-abbey")!.bosses.find((boss) => boss.name === "Flowing Chaos")!;
|
||
expect(flowingChaos.mechanics[0].fidelityLabel).toBe("HealerMan fallback");
|
||
const theSeven = guideByDungeon.get("blackrock-depths")!.bosses.find((boss) => boss.name === "The Seven")!;
|
||
expect(DUNGEON_DEFINITIONS.find((dungeon) => dungeon.id === "blackrock-depths")
|
||
?.entities[theSeven.entityId!].combat?.serverEntry).toBe(9_039);
|
||
const escape = guideByDungeon.get("halls-of-reflection")!.bosses
|
||
.find((boss) => boss.name === "Escape from the Lich King")!;
|
||
expect(DUNGEON_DEFINITIONS.find((dungeon) => dungeon.id === "halls-of-reflection")
|
||
?.entities[escape.entityId!].combat?.serverEntry).toBe(36_954);
|
||
expect(escape.mechanics.length).toBeGreaterThan(0);
|
||
});
|
||
});
|