146 lines
6.4 KiB
TypeScript
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]);
|
|
});
|
|
});
|