120 lines
7.7 KiB
TypeScript
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]);
|
|
});
|
|
});
|