import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, type AbilityDefinition } from "./abilityCatalog"; import { useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility, } from "./partyRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; function playerWhirlwind(state: ReturnType): AbilityDefinition { const ability = state.abilities.find((candidate) => candidate.dbcSpellId === 490133); if (!ability) throw new Error("RuneWaker Warrior Whirlwind is missing from Knight/Warrior abilities."); return ability; } describe("RuneWaker Knight/Warrior Deadly Whirlwind integration", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); }); it("reduces player Whirlwind to 40 Rage and a 10-second cooldown when Deadly Whirlwind is learned", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 80, }); const initial = useCombatStore.getState(); const whirlwind = playerWhirlwind(initial); expect(initial.abilities.find((ability) => ability.dbcSpellId === 491639)).toMatchObject({ classId: "rom-knight", secondaryClassId: "rom-warrior", passive: true, }); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 40 }, maxResourcePools: { ...state.maxResourcePools, rage: 100 }, })); useCombatStore.getState().registerMob("knight-whirlwind-target", { name: "Whirlwind Target", maxHealth: 100_000, xpReward: 0, }); setMobPosition("knight-whirlwind-target", [3, 0, 0]); useCombatStore.getState().selectMob("knight-whirlwind-target"); const result = useCombatStore.getState().castAbility(whirlwind.id, undefined, 1_000); expect(result).toMatchObject({ ok: true }); const final = useCombatStore.getState(); expect(final.resourcePools.rage).toBe(0); expect(final.cooldowns[whirlwind.id]).toBe(11_000); expect(final.combatEvents.some((event) => event.kind === "damage" && event.abilityId === whirlwind.id && event.targetActorId === "knight-whirlwind-target" && event.effectiveAmount > 0)).toBe(true); }); it("uses the learned paired passive for companion Whirlwind cost and cooldown", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 }); const knightSeed = createConfiguredPartyMember({ name: "Knight Warrior", classId: "rom-knight", secondaryClassId: "rom-warrior", specialization: "Primary", role: "damage", }, 80, 0, "deadly-whirlwind-party"); const abilities = abilitiesForCharacter("rom-knight", "rom-warrior"); const whirlwind = abilities.find((ability) => ability.dbcSpellId === 490133)!; const knight = { ...knightSeed, resourcePools: { ...knightSeed.resourcePools, rage: 40 }, cooldowns: Object.fromEntries(abilities .filter((ability) => ability.id !== whirlwind.id) .map((ability) => [ability.id, 60_000])), }; usePartyStore.setState({ members: [knight], command: "attack" }); useCombatStore.getState().synchronizeActors(); registerPartyRuntimePosition(knight.id, [0, 0, 0]); useCombatStore.getState().registerMob("party-knight-whirlwind-target", { name: "Party Whirlwind Target", maxHealth: 100_000, xpReward: 0, }); // Keep the target inside Whirlwind's area but outside the companion's melee range // so its autoattack cannot add Rage after the discounted spell is spent. setMobPosition("party-knight-whirlwind-target", [10, 0, 0]); useCombatStore.getState().damageMob("party-knight-whirlwind-target", 1, 100); updatePartyRuntimeTargetVisibility(knight.id, "party-knight-whirlwind-target", true); advanceDungeonPartyCombat(1_000); const finalMember = usePartyStore.getState().members.find((member) => member.id === knight.id)!; expect(finalMember.resourcePools.rage).toBe(0); expect(finalMember.cooldowns[whirlwind.id]).toBe(11_000); expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "damage" && event.sourceActorId === knight.id && event.abilityId === whirlwind.id && event.targetActorId === "party-knight-whirlwind-target" && event.effectiveAmount > 0)).toBe(true); }); });