Files
healer-man/src/game/romNativeEarlyHealerReserve.test.ts
2026-10-04 13:13:35 -04:00

134 lines
5.4 KiB
TypeScript

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> = {}): 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();
});