import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForClass, abilityAtLevel } from "./abilityCatalog"; import { equipmentAdjustedCombatAmount, resolvedAbilityCosts, useCombatStore } from "./combatStore"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { usePartyStore } from "./partyStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { baseResourcesFor } from "./wotlkStats"; import { companionActorFromMember } from "./combatActorAuthority"; import { BenchmarkController } from "./benchmark/benchmarkController"; import { benchmarkScenarioById } from "./benchmark/benchmarkScenarios"; describe("CoA healer resources and recipients", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); }); it("charges base mana independently of the boosted pool and equipment", () => { useCombatStore.getState().initializeCharacter({ classId: "chronomancer", level: 80 }); const state = useCombatStore.getState(); const heal = abilityAtLevel(abilitiesForClass("chronomancer").find(a => a.name === "Reverse Wound")!, 80); const small = resolvedAbilityCosts(heal, [], { ...state, maxResource: 1_000 }); const large = resolvedAbilityCosts(heal, [], { ...state, maxResource: 50_000 }); expect(small).toEqual(large); expect(small).toEqual([{ resource: "mana", amount: Math.round(baseResourcesFor("chronomancer", 80).mana * heal.cost.amount / 100) }]); expect(resolvedAbilityCosts({ ...heal, costs: undefined, cost: { resource: "health", amount: 8, percentage: true } }, [], state)) .toEqual([{ resource: "health", amount: Math.round(state.maxHealth * .08) }]); }); it("preserves Tinker's secondary mana for both the player and companion actor", () => { useCombatStore.getState().initializeCharacter({ classId: "tinker", role: "healer", level: 80 }); const state = useCombatStore.getState(); usePartyStore.getState().initializeParty("tinker-mana", 80, state.health); const member = { ...usePartyStore.getState().members[0], classId: "tinker" as const, raceId: state.raceId, level: 80, resourceType: "energy" as const, maxResource: state.maxResource, resourcePools: state.resourcePools, resourceMaximums: state.maxResourcePools, gearStats: state.gearStats }; const actor = companionActorFromMember(member); expect(state.maxResourcePools.mana).toBeGreaterThan(1_000); expect(actor.resources.mana.current).toBe(state.resourcePools.mana); expect(actor.resources.mana.maximum).toBe(state.maxResourcePools.mana); }); it("sends Protection From Light and its periodic heal to the selected ally", () => { useCombatStore.getState().initializeCharacter({ classId: "cultist", role: "healer", level: 80 }); usePartyStore.getState().initializeParty("protection-heal", 80, useCombatStore.getState().health, "healer", 2); const member = usePartyStore.getState().members[0]; registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().damageMember(member.id, 500); usePartyStore.getState().selectMember(member.id); const before = usePartyStore.getState().members[0].health; const spell = abilitiesForClass("cultist").find(a => a.name === "Protection From Light")!; expect(spell.target).toBe("friendly"); expect(useCombatStore.getState().castAbility(spell.id, undefined, 1_000).ok).toBe(true); useCombatStore.getState().tick(.1, 3_000); expect(usePartyStore.getState().members[0].health).toBeGreaterThan(before); expect(useCombatStore.getState().effects.some(e => e.kind === "hot" && e.targetId === member.id)).toBe(true); }); it("starts Bloodmage weapon attacks with its hostile life-stealing bite", () => { useCombatStore.getState().initializeCharacter({ classId: "bloodmage", role: "healer", level: 8 }); usePartyStore.getState().initializeParty("blood-rage", 8, useCombatStore.getState().health, "healer", 2); useCombatStore.getState().registerMob("boss", { name: "Boss", boss: true, level: 8, maxHealth: 100_000 }); setMobPosition("boss", [2, 0, 0]); useCombatStore.getState().selectMob("boss"); const bite = abilitiesForClass("bloodmage").find(a => a.name === "Ravenous Bite (Packleader)")!; expect(useCombatStore.getState().castAbility(bite.id, undefined, 1_000).ok).toBe(true); expect(useCombatStore.getState().autoAttackTargetId).toBe("boss"); useCombatStore.getState().tick(.1, 2_500); expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThan(0); }); it("uses Sanguine Mend's authored spell power, attack power and Spirit coefficients", () => { const spell = abilityAtLevel(abilitiesForClass("bloodmage").find(a => a.name === "Sanguine Mend")!, 80); const effect = spell.effects.find(e => e.kind === "heal")!; expect(effect.kind).toBe("heal"); if (effect.kind !== "heal") throw new Error("Missing heal"); const amount = (gear: typeof EMPTY_ITEM_STATS) => equipmentAdjustedCombatAmount(effect, 80, gear, "bloodmage", spell); const base = amount(EMPTY_ITEM_STATS); expect(amount({ ...EMPTY_ITEM_STATS, spellPower: 100 }) - base).toBe(58); expect(amount({ ...EMPTY_ITEM_STATS, attackPower: 100 }) - base).toBe(20); // Human Spirit also applies to the added Spirit. expect(amount({ ...EMPTY_ITEM_STATS, spirit: 100 }) - base).toBe(26); }); it("lets a maintained heal tick while the caster is at full health", () => { useCombatStore.getState().initializeCharacter({ classId: "druid", role: "healer", level: 8 }); usePartyStore.getState().initializeParty("hot-maintenance", 8, useCombatStore.getState().health, "healer", 2); const member = usePartyStore.getState().members[0]; registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().damageMember(member.id, member.maxHealth * .5); const before = usePartyStore.getState().members[0].health; useCombatStore.setState({ abilities: abilitiesForClass("druid").filter(a => a.name === "Rejuvenation") }); const controller = new BenchmarkController(); const scenario = benchmarkScenarioById("heal-stacked-party")!; expect(controller.act(1_000, "healer", scenario, false).cast).toBe(true); const nextTick = useCombatStore.getState().effects[0].nextTickAt; expect(controller.act(2_500, "healer", scenario, false).cast).toBe(false); expect(useCombatStore.getState().effects[0].nextTickAt).toBe(nextTick); useCombatStore.getState().tick(.1, nextTick); expect(usePartyStore.getState().members[0].health).toBeGreaterThan(before); }); it("heals nearby allies with Mending Mist while respecting its radius", () => { useCombatStore.getState().initializeCharacter({ classId: "venomancer", role: "healer", level: 80 }); usePartyStore.getState().initializeParty("mist-radius", 80, useCombatStore.getState().health, "healer", 4); const members = usePartyStore.getState().members; for (const [index, member] of members.entries()) { registerPartyRuntimePosition(member.id, [index === 2 ? 100 : 3 + index, 0, 0]); usePartyStore.getState().damageMember(member.id, 1_000); } const before = usePartyStore.getState().members.map(m => m.health); const spell = abilitiesForClass("venomancer").find(a => a.name === "Mending Mist")!; expect(spell.target).toBe("self"); expect(useCombatStore.getState().castAbility(spell.id, undefined, 1_000).ok).toBe(true); useCombatStore.getState().tick(.1, 3_000); useCombatStore.getState().tick(.1, 4_000); expect(usePartyStore.getState().members[0].health).toBeGreaterThan(before[0]); expect(usePartyStore.getState().members[1].health).toBeGreaterThan(before[1]); expect(usePartyStore.getState().members[2].health).toBe(before[2]); }); });