import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForClass, preloadAbilityCatalogsForClasses } from "./abilityCatalog"; import { applyAura } from "./combatAuras"; 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"; import { WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG } from "./wow335HunterRuntime"; import { partyAbilityCosts, partyAbilityIsReady } from "./partyAbilityAi"; describe("WoW Hunter Lock and Load companion runtime", () => { beforeEach(async () => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); await preloadAbilityCatalogsForClasses(["hunter"]); useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 }); usePartyStore.getState().initializeParty( "hunter-lock-and-load-party", 80, useCombatStore.getState().health, "damage", 1, ); }); it("lets a companion spend two free Arcane Shots through its cooldown and mana checks", () => { const arcaneShot = abilitiesForClass("hunter").find((ability) => ability.name === "Arcane Shot")!; const hunterAbilities = abilitiesForClass("hunter"); const hunterBase = createConfiguredPartyMember({ name: "Survival Hunter", classId: "hunter", specialization: "Survival", specializationId: "hunter:survival", talentTreeId: "hunter-survival", role: "damage", talentRanks: { "talent-1306": 3 }, }, 80, 0, "hunter-lock-and-load-party"); const hunter = { ...hunterBase, resource: 0, resourcePools: { ...hunterBase.resourcePools, mana: 0 }, lastResourceSpentAt: {}, cooldowns: Object.fromEntries(hunterAbilities.map((ability) => [ability.id, 60_000])), globalCooldownEndsAt: 0, nextActionAt: 0, status: "ready" as const, }; usePartyStore.setState({ members: [hunter], command: "attack" }); registerPartyRuntimePosition(hunter.id, [0, 0, 0]); useCombatStore.getState().synchronizeActors(); useCombatStore.getState().registerMob("hunter-lock-and-load-party-target", { name: "Training Target", maxHealth: 100_000, xpReward: 0, }); setMobPosition("hunter-lock-and-load-party-target", [0, 0, 12]); useCombatStore.getState().damageMob("hunter-lock-and-load-party-target", 1, 500); updatePartyRuntimeTargetVisibility(hunter.id, "hunter-lock-and-load-party-target", true); useCombatStore.setState((state) => ({ auras: applyAura(state.auras, { definition: { id: "wow335:hunter:lock-and-load-ready", name: "Lock and Load", disposition: "buff", durationMs: 15_000, maxStacks: 2, initialStacks: 2, stackBehavior: "replace", utilityTags: [WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG], }, sourceId: hunter.id, targetId: hunter.id, now: 1_000, }).auras, })); expect(partyAbilityIsReady(hunter, arcaneShot, 1_000)).toBe(true); expect(partyAbilityCosts(hunter, arcaneShot, 1_000)).toEqual(expect.arrayContaining([ expect.objectContaining({ resource: "mana", amount: 0 }), ])); advanceDungeonPartyCombat(1_000); expect(usePartyStore.getState().members[0]?.cooldowns[arcaneShot.id]).toBe(0); expect(usePartyStore.getState().members[0]?.lastResourceSpentAt.mana).toBe(0); expect(useCombatStore.getState().auras.find((aura) => ( aura.targetId === hunter.id && aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG) ))?.stacks).toBe(1); advanceDungeonPartyCombat(3_000); expect(usePartyStore.getState().members[0]?.cooldowns[arcaneShot.id]).toBe(0); expect(usePartyStore.getState().members[0]?.lastResourceSpentAt.mana).toBe(0); expect(useCombatStore.getState().auras.some((aura) => ( aura.targetId === hunter.id && aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG) ))).toBe(false); }); it("creates a companion Freezing Arrow trap that procs the companion's talent", () => { const hunter = createConfiguredPartyMember({ name: "Survival Hunter", classId: "hunter", specialization: "Survival", specializationId: "hunter:survival", talentTreeId: "hunter-survival", role: "damage", talentRanks: { "talent-1306": 3 }, }, 80, 0, "hunter-freezing-arrow-party"); usePartyStore.setState({ members: [hunter] }); registerPartyRuntimePosition(hunter.id, [0, 0, 0]); useCombatStore.getState().synchronizeActors(); useCombatStore.getState().registerMob("hunter-freezing-arrow-party-target", { name: "Training Target", maxHealth: 100_000, xpReward: 0, }); setMobPosition("hunter-freezing-arrow-party-target", [0, 0, 12]); const freezingArrow = abilitiesForClass("hunter").find((ability) => ability.name === "Freezing Arrow")!; const areaId = useCombatStore.getState().createActorAreaEffect( hunter.id, freezingArrow, [0, 0, 0], 1_000, ); expect(areaId).toBeTruthy(); setMobPosition("hunter-freezing-arrow-party-target", [0, 0, 1]); useCombatStore.getState().tick(1, 2_000); expect(useCombatStore.getState().mobs["hunter-freezing-arrow-party-target"]?.statuses) .toEqual(expect.arrayContaining([expect.objectContaining({ kind: "freeze", sourceActorId: hunter.id })])); expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([ expect.objectContaining({ targetId: hunter.id, stacks: 2, expiresAt: 17_000 }), ])); }); });