import { describe, expect, it } from "vitest"; import { HEALER_CLASSES, HEALER_CLASS_ORDER } from "./healers"; import { BASE_MANA_POOL, MANA_REGEN_PER_SECOND } from "./mana"; import type { AbilitySlotId, HealerClassId } from "./types"; const LONG_DUAL_BOSS_SECONDS = 100; const MINIMUM_ENDING_RESERVE = 20; const MAXIMUM_ENDING_RESERVE = 50; /** * High-pressure reference rotations for the longest intended dual-boss fight. * Counts include spot healing, maintenance effects, four cleanses, repeated * group recovery, and both available one-minute cooldown casts. */ const REFERENCE_ROTATIONS = { priest: { ability1: 8, ability2: 8, ability3: 5, ability4: 4, ability5: 5, ability6: 2 }, druid: { ability1: 7, ability2: 9, ability3: 9, ability4: 4, ability5: 5, ability6: 2 }, shaman: { ability1: 7, ability2: 9, ability3: 4, ability4: 4, ability5: 6, ability6: 2 }, paladin: { ability1: 10, ability2: 20, ability3: 4, ability4: 4, ability5: 6, ability6: 2 }, chronomancer: { ability1: 7, ability2: 8, ability3: 8, ability4: 4, ability5: 5, ability6: 2 }, } as const satisfies Record>; function rotationManaCost(classId: HealerClassId): number { const abilities = HEALER_CLASSES[classId].abilities; return Object.entries(REFERENCE_ROTATIONS[classId]).reduce( (total, [slotId, casts]) => total + abilities[slotId as AbilitySlotId].mana * casts, 0, ); } describe("dual-boss healer mana balance", () => { const availableMana = BASE_MANA_POOL + MANA_REGEN_PER_SECOND * LONG_DUAL_BOSS_SECONDS; it.each(HEALER_CLASS_ORDER)("funds %s's high-pressure 100-second rotation with a useful reserve", (classId) => { const rotation = REFERENCE_ROTATIONS[classId]; const abilities = HEALER_CLASSES[classId].abilities; const totalCasts = Object.values(rotation).reduce((total, casts) => total + casts, 0); const remainingMana = availableMana - rotationManaCost(classId); expect(totalCasts).toBeLessThanOrEqual(LONG_DUAL_BOSS_SECONDS / 0.5); expect(Object.values(rotation).every((casts) => casts > 0)).toBe(true); for (const [slotId, casts] of Object.entries(rotation)) { const cooldown = abilities[slotId as AbilitySlotId].cooldown; if (cooldown <= 0) continue; const maximumCasts = Math.floor((LONG_DUAL_BOSS_SECONDS - Number.EPSILON) / cooldown) + 1; expect(casts).toBeLessThanOrEqual(maximumCasts); } expect(remainingMana).toBeGreaterThanOrEqual(MINIMUM_ENDING_RESERVE); expect(remainingMana).toBeLessThanOrEqual(MAXIMUM_ENDING_RESERVE); }); it("keeps reference rotation costs close enough that no class gets a dominant mana advantage", () => { const costs = HEALER_CLASS_ORDER.map(rotationManaCost); expect(Math.max(...costs) - Math.min(...costs)).toBeLessThanOrEqual(25); }); it("detects why the previous 100 mana pool was insufficient", () => { const previousBudget = 100 + MANA_REGEN_PER_SECOND * LONG_DUAL_BOSS_SECONDS; for (const classId of HEALER_CLASS_ORDER) { expect(rotationManaCost(classId)).toBeGreaterThan(previousBudget); } }); });