Files
healer-man/src/game/wow335ShamanProcRuntime.test.ts
T
2026-09-28 22:46:45 -04:00

153 lines
6.8 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
import { usePartyStore } from "./partyStore";
function shamanAbility(name: string) {
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
expect(ability, `Shaman ${name}`).toBeDefined();
return ability!;
}
function finishCast(abilityId: string, targetId: string | undefined, startedAt: number): void {
const requestTargetId = targetId === PLAYER_AURA_ENTITY_ID ? null : targetId;
const result = useCombatStore.getState().castAbility(
abilityId,
requestTargetId !== undefined
? { targetActorId: requestTargetId, now: startedAt }
: { now: startedAt },
);
expect(result.ok, JSON.stringify(result)).toBe(true);
const cast = useCombatStore.getState().activeCast;
if (cast) useCombatStore.getState().tick(0.1, cast.completesAt);
}
describe("WoW Shaman source-backed proc talents", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.getState().initializeCharacter({ classId: "shaman", level: 80, xp: 0 });
useCombatStore.getState().resetSettings();
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty("wow-shaman-proc-tests", 80, useCombatStore.getState().health);
for (const [index, member] of usePartyStore.getState().members.entries()) {
registerPartyRuntimePosition(member.id, [4 + index, 0, 0]);
}
});
it.each([
[1, 500],
[2, 1_000],
[3, 1_500],
])("reduces the shared Shock cooldown by %i rank(s) (%i ms) with Reverberation", (rank, reductionMs) => {
const flameShock = shamanAbility("Flame Shock");
const shockAbilities = useCombatStore.getState().abilities.filter((ability) => (
ability.cooldownGroup === "shaman-shock"
));
expect(shockAbilities.length).toBeGreaterThan(1);
const target = `reverberation-target-${rank}`;
useCombatStore.getState().registerMob(target, {
name: "Reverberation target",
level: 80,
maxHealth: 100_000,
xpReward: 0,
});
setMobPosition(target, [8, 0, 0]);
useCombatStore.getState().selectMob(target);
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-575": rank },
}));
const result = useCombatStore.getState().castAbility(flameShock.id, undefined, 1_000);
expect(result.ok, JSON.stringify(result)).toBe(true);
const cast = useCombatStore.getState().activeCast;
if (cast) useCombatStore.getState().tick(0.1, cast.completesAt);
const expectedReadyAt = 1_000 + 6_000 - reductionMs;
for (const ability of shockAbilities) {
expect(useCombatStore.getState().cooldowns[ability.id], ability.name).toBe(expectedReadyAt);
}
});
it("uses the ranked Ancestral Healing reduction and heals the lowest-health ally from Ancestral Awakening", () => {
const members = usePartyStore.getState().members;
const mostInjured = members[0]!;
usePartyStore.setState((state) => ({
members: state.members.map((member) => member.id === mostInjured.id
? { ...member, health: Math.round(member.maxHealth * 0.4) }
: member),
}));
useCombatStore.setState((state) => ({
health: state.maxHealth - 1,
gearStats: { ...state.gearStats, criticalStrikeRating: 100_000 },
talentRanks: { ...state.talentRanks, "talent-2061": 3, "talent-581": 3 },
}));
const heal = shamanAbility("Healing Wave");
finishCast(heal.id, PLAYER_AURA_ENTITY_ID, 1_000);
const events = useCombatStore.getState().combatEvents;
const directHeal = events.find((event) => event.kind === "healing" && event.abilityId === heal.id);
expect(directHeal?.critical).toBe(true);
const awakeningHeal = events.find((event) => (
event.kind === "healing" && event.abilityId === "wow335-shaman-ancestral-awakening"
));
expect(awakeningHeal).toMatchObject({ targetActorId: mostInjured.id, school: "nature" });
expect(awakeningHeal?.rawAmount).toBe(Math.round((directHeal?.rawAmount ?? 0) * 0.3));
expect(usePartyStore.getState().members.find((member) => member.id === mostInjured.id)?.health)
.toBeGreaterThan(Math.round(mostInjured.maxHealth * 0.4));
const ancestralHealing = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-shaman-ancestral-healing")
));
expect(ancestralHealing).toMatchObject({ expiresAt: directHeal!.occurredAt + 15_000 });
expect(ancestralHealing?.definition.modifiers).toContainEqual({
kind: "damage",
direction: "taken",
school: "physical",
operation: "percent",
value: -0.1,
});
});
it("reduces Elemental Mastery and Thunderstorm cooldowns on damaging Lightning Bolt hits", () => {
const state = useCombatStore.getState();
const lightningBolt = shamanAbility("Lightning Bolt");
const sourceCooldownAbilities = ["Elemental Mastery", "Thunderstorm"].map((name) => (
WOW335_ABILITIES_BY_CLASS.shaman.find((ability) => ability.name === name)
));
expect(sourceCooldownAbilities.every(Boolean)).toBe(true);
const cooldownAbilities = sourceCooldownAbilities as NonNullable<typeof sourceCooldownAbilities[number]>[];
const target = "shamanism-target";
useCombatStore.getState().registerMob(target, { name: "Shamanism target", level: 80, maxHealth: 100_000, xpReward: 0 });
setMobPosition(target, [8, 0, 0]);
useCombatStore.getState().selectMob(target);
useCombatStore.setState((current) => ({
talentRanks: { ...current.talentRanks, "talent-2252": 3 },
abilities: [...current.abilities, ...cooldownAbilities.filter((ability) => (
!current.abilities.some((candidate) => candidate.id === ability.id)
))],
cooldowns: {
...current.cooldowns,
[cooldownAbilities[0]!.id]: 20_000,
[cooldownAbilities[1]!.id]: 20_000,
},
}));
const result = useCombatStore.getState().castAbility(lightningBolt.id, undefined, 1_000);
expect(result.ok, JSON.stringify(result)).toBe(true);
const cast = useCombatStore.getState().activeCast;
if (cast) useCombatStore.getState().tick(0.1, cast.completesAt);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "damage" && event.abilityId === lightningBolt.id && event.effectiveAmount > 0
))).toBe(true);
expect(useCombatStore.getState().cooldowns[cooldownAbilities[0]!.id]).toBe(19_000);
expect(useCombatStore.getState().cooldowns[cooldownAbilities[1]!.id]).toBe(19_000);
});
});