Files
healer-man/src/game/coaHealerRuntime.test.ts
T

120 lines
7.7 KiB
TypeScript

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