64 lines
3.0 KiB
TypeScript
64 lines
3.0 KiB
TypeScript
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<HealerClassId, Record<AbilitySlotId, number>>;
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|