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

146 lines
6.4 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { abilityTalentNodeId, type AbilityDefinition } from "./abilityCatalog";
import { WOW335_GLYPH_OF_SWIFTMEND_ID } from "./wow335GlyphCatalog";
import { talentModifiersForRanks } from "./talentModifiers";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { useCombatStore, type TimedCombatEffect } from "./combatStore";
function prepareSwiftmend(): AbilityDefinition {
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
useCombatStore.setState((state) => ({ health: 1_000, resource: state.maxResource }));
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === "Swiftmend");
if (!ability) throw new Error("Druid Swiftmend is missing from the learned ability catalog.");
const talentId = abilityTalentNodeId(ability);
if (!talentId) throw new Error("Druid Swiftmend has no active talent node.");
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, [talentId]: 1 } }));
return ability;
}
function hot(
sourceName: "Rejuvenation" | "Regrowth",
sourceActorId: string,
tickHealing: number,
remainingTicks: number,
nextTickAt: number,
id: string,
fixedAmount?: number,
): TimedCombatEffect {
const sourceAbility = useCombatStore.getState().abilities.find((ability) => ability.name === sourceName);
if (!sourceAbility) throw new Error(`Druid ${sourceName} is missing from the learned ability catalog.`);
return {
id,
kind: "hot",
sourceAbilityId: sourceAbility.id,
sourceActorId,
targetId: null,
coefficient: 0,
basePoints: tickHealing,
dieSides: 1,
pointsPerLevel: 0,
sourceLevel: 80,
...(fixedAmount === undefined ? {} : { fixedAmount }),
remainingTicks,
intervalMs: 3_000,
nextTickAt,
};
}
function castSwiftmend(ability: AbilityDefinition, now = 1_000) {
const result = useCombatStore.getState().castAbility(ability.id, { targetActorId: null, now });
expect(result.ok).toBe(true);
const event = useCombatStore.getState().combatEvents.filter((candidate) => (
candidate.kind === "healing" && candidate.abilityId === ability.id
)).at(-1);
if (!event || event.kind !== "healing") throw new Error("Swiftmend did not record its healing event.");
return event;
}
function expectCriticalAdjustedRawHealing(rawAmount: number, critical: boolean, baseAmount: number): void {
expect(rawAmount).toBe(baseAmount * (critical ? 1.5 : 1));
}
describe("WoW Druid active talent runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
});
it("lets Nature's Reach extend a cast-time damaging spell to its generated rank range", () => {
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
const wrath = useCombatStore.getState().abilities.find((candidate) => candidate.name === "Wrath");
if (!wrath) throw new Error("Druid Wrath is missing from the learned ability catalog.");
useCombatStore.getState().registerMob("natures-reach-target", { name: "Nature's Reach target", maxHealth: 100_000 });
setMobPosition("natures-reach-target", [35, 0, 0]);
useCombatStore.getState().selectMob("natures-reach-target");
expect(useCombatStore.getState().castAbility(wrath.id, {
targetActorId: "natures-reach-target",
now: 1_000,
})).toMatchObject({ ok: false, reason: "out-of-range" });
const talentRanks = { ...useCombatStore.getState().talentRanks, "talent-764": 3 };
useCombatStore.setState({
talentRanks,
talentModifiers: talentModifiersForRanks(talentRanks, "druid"),
});
const extendedCast = useCombatStore.getState().castAbility(wrath.id, {
targetActorId: "natures-reach-target",
now: 2_000,
});
expect(extendedCast.ok).toBe(true);
});
it.each([
["Rejuvenation", 100, 5, 400],
["Regrowth", 150, 7, 900],
] as const)("Swiftmend uses its source 12/18-second amount for %s", (sourceName, tickHealing, remainingTicks, expected) => {
const swiftmend = prepareSwiftmend();
const sourceHot = hot(sourceName, PLAYER_AGGRO_ID, tickHealing, remainingTicks, 4_000, "swiftmend-source-hot");
useCombatStore.setState({ effects: [sourceHot] });
const event = castSwiftmend(swiftmend);
expectCriticalAdjustedRawHealing(event.rawAmount, event.critical, expected);
expect(useCombatStore.getState().effects).toEqual([]);
});
it("prefers its own Hot over a shorter foreign Hot", () => {
const swiftmend = prepareSwiftmend();
const foreignShort = hot("Rejuvenation", "party-druid-short", 25, 1, 1_500, "swiftmend-foreign-short", 25);
const casterHot = hot("Regrowth", PLAYER_AGGRO_ID, 100, 7, 16_000, "swiftmend-caster-long");
useCombatStore.setState({ effects: [foreignShort, casterHot] });
const event = castSwiftmend(swiftmend);
expectCriticalAdjustedRawHealing(event.rawAmount, event.critical, 600);
expect(useCombatStore.getState().effects.map((effect) => effect.id)).toEqual([foreignShort.id]);
});
it("uses the shortest foreign Rejuvenation or Regrowth when no caster Hot exists", () => {
const swiftmend = prepareSwiftmend();
const longerRegrowth = hot("Regrowth", "party-druid-long", 50, 7, 4_000, "swiftmend-foreign-long", 50);
const shorterRejuvenation = hot("Rejuvenation", "party-druid-short", 100, 5, 4_000, "swiftmend-foreign-short", 100);
useCombatStore.setState({ effects: [longerRegrowth, shorterRejuvenation] });
const event = castSwiftmend(swiftmend);
expectCriticalAdjustedRawHealing(event.rawAmount, event.critical, 400);
expect(useCombatStore.getState().effects.map((effect) => effect.id)).toEqual([longerRegrowth.id]);
});
it("preserves the selected HoT when Glyph of Swiftmend is active", () => {
const swiftmend = prepareSwiftmend();
const sourceHot = hot("Rejuvenation", PLAYER_AGGRO_ID, 100, 5, 4_000, "swiftmend-glyph-hot");
useCombatStore.setState({ effects: [sourceHot] });
expect(useCombatStore.getState().setGlyphActive(WOW335_GLYPH_OF_SWIFTMEND_ID, true)).toBe(true);
const event = castSwiftmend(swiftmend);
expectCriticalAdjustedRawHealing(event.rawAmount, event.critical, 400);
expect(useCombatStore.getState().glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
expect(useCombatStore.getState().effects).toEqual([sourceHot]);
});
});