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, }), ])); }); });