forked from phenom/healer-man
134 lines
5.4 KiB
TypeScript
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();
|
|
});
|