import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForClass, abilityAtLevel, learnedAbilityForBuild, type AbilityDefinition, } from "./abilityCatalog"; import { useCombatStore } from "./combatStore"; import { evaluateCombatRotation, nativeEarlyPriestHealReserveBlocksAbility, type RotationContext, } from "./combatRotation"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry"; import { healingAbilityCanAidOthers } from "./healingAbilityRules"; import { BenchmarkController } from "./benchmark/benchmarkController"; import { BENCHMARK_SCENARIOS } from "./benchmark/benchmarkScenarios"; const urgentHeal = abilityAtLevel(abilitiesForClass("rom-priest").find((ability) => ability.name === "Urgent Heal")!, 1); const risingTide = abilityAtLevel(abilitiesForClass("rom-priest").find((ability) => ability.name === "Rising Tide")!, 1); function manaCost(ability: AbilityDefinition): number { return (ability.costs?.length ? ability.costs : [ability.cost]) .filter((cost) => cost.resource === "mana") .reduce((total, cost) => total + cost.amount, 0); } function rotationContext(overrides: Partial = {}): RotationContext { const mana = overrides.resources?.mana ?? 36; return { now: 1_000, classId: "rom-priest", statRuleset: "rom", role: "healer", level: 1, health: 100, maxHealth: 100, selfId: "priest", resources: { mana }, maximumResources: { mana: 123 }, primaryResource: "mana", comboPoints: 0, comboTargetId: null, runes: [], abilities: [risingTide], learnedAbilities: [urgentHeal, risingTide], manaCostForAbility: manaCost, canUseAbility: (ability) => manaCost(ability) <= mana, hostileTarget: { id: "enemy", health: 1_000, maxHealth: 1_000 }, hostileCount: 1, ...overrides, }; } describe("native early RoM Priest healer Mana reserve", () => { it("keeps the learned direct-heal cost at 25 Mana across the 36/37 spend boundary", () => { expect(manaCost(urgentHeal)).toBe(25); expect(manaCost(risingTide)).toBe(12); const learnedDirectHealCosts = abilitiesForClass("rom-priest") .map((ability) => learnedAbilityForBuild(ability, 1, {})) .filter((ability): ability is AbilityDefinition => ability !== null) .filter((ability) => !ability.passive && (ability.target === "friendly" || ability.target === "any") && healingAbilityCanAidOthers(ability) && ability.effects.some((effect) => effect.kind === "heal")) .map((ability) => `${ability.name}:${manaCost(ability)}`) .sort(); expect(learnedDirectHealCosts).toEqual(["Urgent Heal:25"]); const at36 = rotationContext(); expect(at36.canUseAbility?.(risingTide)).toBe(true); expect(evaluateCombatRotation(at36)).toBeNull(); const at37 = rotationContext({ resources: { mana: 37 } }); expect(evaluateCombatRotation(at37)?.ability.id).toBe(risingTide.id); }); it("uses resolved Mana costs and leaves Urgent Heal's own target checks intact", () => { const expensiveResolvedStrike = rotationContext({ resources: { mana: 38 }, manaCostForAbility: (ability) => manaCost(ability) + (ability.id === risingTide.id ? 2 : 0), canUseAbility: () => true, }); expect(evaluateCombatRotation(expensiveResolvedStrike)).toBeNull(); const heal = evaluateCombatRotation(rotationContext({ resources: { mana: 25 }, abilities: [urgentHeal], friendlyTargets: [{ id: "ally", health: 20, maxHealth: 100, role: "damage" }], })); expect(heal).toMatchObject({ ability: { id: urgentHeal.id }, targetId: "ally" }); const invalidHealTarget = evaluateCombatRotation(rotationContext({ resources: { mana: 25 }, abilities: [urgentHeal], friendlyTargets: [{ id: "fallen-ally", health: 0, maxHealth: 100, fallen: true }], })); expect(invalidHealTarget).toBeNull(); }); it("applies only to sub-10 RoM Priests acting as healers", () => { const base = rotationContext(); expect(nativeEarlyPriestHealReserveBlocksAbility(risingTide, base)).toBe(true); expect(nativeEarlyPriestHealReserveBlocksAbility(risingTide, { ...base, level: 10 })).toBe(false); expect(nativeEarlyPriestHealReserveBlocksAbility(risingTide, { ...base, role: "damage" })).toBe(false); expect(nativeEarlyPriestHealReserveBlocksAbility(risingTide, { ...base, statRuleset: "wow" })).toBe(false); }); it("keeps the real level-one benchmark actor from spending its 36-Mana heal reserve", () => { const combat = useCombatStore.getState(); combat.initializeCharacter({ classId: "rom-priest", statRuleset: "rom", role: "healer", level: 1, raceId: "rom-human" }); combat.setPlayerPosition([0, 0, 0]); combat.registerMob("reserve-target", { name: "Reserve target", level: 1, maxHealth: 100_000, xpReward: 0 }); setMobPosition("reserve-target", [10, 0, 0]); combat.selectMob("reserve-target"); useCombatStore.setState((state) => ({ resource: 36, resourcePools: { ...state.resourcePools, mana: 36 }, })); const controller = new BenchmarkController(); const scenario = { ...BENCHMARK_SCENARIOS[0], hostileTargets: 1 }; const at36 = controller.act(100, "healer", scenario, false); expect(at36.cast).toBe(false); expect(useCombatStore.getState().resourcePools.mana).toBe(36); }); }); beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); });