Files
healer-man/src/game/bossGuide.test.ts
T
2026-09-09 11:21:33 -04:00

198 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});