Files
healer-man/src/game/romPriestChampionHeartRuneEnergyRuntime.test.ts
T
2026-10-02 10:38:12 -04:00

169 lines
6.9 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { targetingRulesForAbility } from "./abilityRules";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
clearPartyRuntimeRegistry,
registerPartyRuntimePosition,
updatePartyRuntimeTargetVisibility,
} from "./partyRuntimeRegistry";
import { useCombatStore } from "./combatStore";
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
import { commitPartyAbilityUse } from "./partyAbilityAi";
import { abilitiesForCharacter } from "./abilityCatalog";
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
const HEART_RUNE_ENERGY_ID = 498612;
const BLESSING_OF_HUMILITY_ID = 490301;
const TARGET_DISTANCES = [2, 15, 30, 45, 65, 80] as const;
const OUTSIDE_DISTANCE = 80.1;
const TEST_NOW = 1_000;
const heartRuneEnergy = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === HEART_RUNE_ENERGY_ID
&& ability.classId === "rom-priest" && ability.secondaryClassId === "rom-champion")!;
function registerTarget(id: string, distance: number): void {
useCombatStore.getState().registerMob(id, {
name: id,
maxHealth: 1_000_000,
level: 1,
xpReward: 0,
});
setMobPosition(id, [distance, 0, 0]);
useCombatStore.getState().damageMob(id, 1, 100);
}
function registerHeartRuneTargets(prefix: string): { insideIds: string[]; outsideId: string } {
const insideIds = TARGET_DISTANCES.map((distance, index) => {
const id = `${prefix}-inside-${index}`;
registerTarget(id, distance);
return id;
});
const outsideId = `${prefix}-outside`;
registerTarget(outsideId, OUTSIDE_DISTANCE);
return { insideIds, outsideId };
}
function abilityDamageTargets(abilityId: string, sourceActorId?: string): string[] {
return useCombatStore.getState().combatEvents
.filter((event) => event.kind === "damage"
&& event.abilityId === abilityId
&& (!sourceActorId || event.sourceActorId === sourceActorId)
&& event.school === "holy"
&& event.effectiveAmount > 0)
.map((event) => event.targetActorId)
.sort();
}
describe("RoM Priest/Champion Heart Rune Energy runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetPartyRuntimeClock();
useCombatStore.getState().initializeCharacter({
classId: "rom-priest",
secondaryClassId: "rom-champion",
raceId: "rom-human",
level: 80,
role: "damage",
});
usePartyStore.getState().initializeParty("rom-priest-heart-rune-energy", 80,
useCombatStore.getState().health, "damage", 2, "rom");
});
it("executes the caster-centered 80-radius Light hit for every catalog-eligible target and reduces Blessing cooldown", () => {
const { insideIds, outsideId } = registerHeartRuneTargets("heart-rune-player");
const blessing = useCombatStore.getState().abilities.find((ability) => (
ability.dbcSpellId === BLESSING_OF_HUMILITY_ID
))!;
useCombatStore.setState((state) => ({
resourcePools: { ...state.resourcePools, rage: 50 },
cooldowns: { ...state.cooldowns, [blessing.id]: TEST_NOW + 60_000 },
}));
useCombatStore.getState().selectMob(insideIds[0]!);
const result = useCombatStore.getState().castAbility(heartRuneEnergy.id, undefined, TEST_NOW);
expect(result).toMatchObject({ ok: true });
expect(useCombatStore.getState().resourcePools.rage).toBe(35);
expect(useCombatStore.getState().cooldowns[heartRuneEnergy.id]).toBe(TEST_NOW + 12_000);
expect(useCombatStore.getState().cooldowns[blessing.id]).toBe(TEST_NOW + 45_000);
expect(abilityDamageTargets(heartRuneEnergy.id)).toEqual(insideIds.sort());
expect(abilityDamageTargets(heartRuneEnergy.id)).not.toContain(outsideId);
});
it("executes for generated Priest/Champion companions with the same radius, no configured target cap, and cooldown rider", () => {
const { insideIds, outsideId } = registerHeartRuneTargets("heart-rune-companion");
const seed = "rom-priest-heart-rune-energy-companion";
const blessing = abilitiesForCharacter("rom-priest", "rom-champion")
.find((ability) => ability.dbcSpellId === BLESSING_OF_HUMILITY_ID)!;
const member = createConfiguredPartyMember({
name: "Priest Champion",
classId: "rom-priest",
secondaryClassId: "rom-champion",
specialization: "Primary",
role: "damage",
}, 80, 0, seed);
const fundedMember = {
...member,
resourcePools: { ...member.resourcePools, rage: 50 },
cooldowns: { ...member.cooldowns, [blessing.id]: TEST_NOW + 60_000 },
};
const committedMember = commitPartyAbilityUse(fundedMember, heartRuneEnergy, TEST_NOW);
expect(committedMember).not.toBeNull();
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2, "rom");
usePartyStore.getState().setCommand("attack");
usePartyStore.setState({ members: [{
...committedMember!,
activeCast: {
abilityId: heartRuneEnergy.id,
school: "holy",
mode: "cast",
startedAt: TEST_NOW,
completesAt: TEST_NOW,
originalDurationMs: 0,
pushbackHits: 0,
origin: [0, 0, 0],
targetId: insideIds[0]!,
},
activeCastTargetId: insideIds[0]!,
status: "attacking",
}] });
registerPartyRuntimePosition(member.id, [0, 0, 0]);
for (const id of [...insideIds, outsideId]) updatePartyRuntimeTargetVisibility(member.id, id, true);
useCombatStore.getState().synchronizeActors();
advanceDungeonPartyCombat(TEST_NOW);
const updatedMember = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!;
expect(updatedMember.resourcePools.rage).toBe(35);
expect(updatedMember.cooldowns[heartRuneEnergy.id]).toBe(TEST_NOW + 12_000);
expect(updatedMember.cooldowns[blessing.id]).toBe(TEST_NOW + 45_000);
expect(abilityDamageTargets(heartRuneEnergy.id, member.id)).toEqual(insideIds.sort());
expect(abilityDamageTargets(heartRuneEnergy.id, member.id)).not.toContain(outsideId);
});
it("keeps the current source radius and leaves target count uncapped because CoA provides no cap", () => {
const targeting = targetingRulesForAbility(heartRuneEnergy);
expect(heartRuneEnergy).toMatchObject({
target: "self",
range: { max: 150 },
radius: 80,
castMode: "instant",
castTimeMs: 0,
cooldownMs: 12_000,
cost: { resource: "rage", amount: 15 },
});
expect(targeting).toMatchObject({ shape: "circle", radius: 80 });
expect(targeting.maximumTargets).toBeUndefined();
expect(heartRuneEnergy.effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "damage", coefficient: 9.751, school: "holy" }),
expect.objectContaining({
kind: "cooldown-reset",
mode: "reduce",
abilityName: "Blessing of Humility",
amountMs: 15_000,
}),
]));
});
});