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