import { beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { ROM_ALL_ABILITIES } from "./romAbilityCatalog"; import { ROM_DRUID_MAGE_INSPIRATION_AURA_TAG, romDruidMageInspirationApplySpendStack, romDruidMageInspirationHastePercent, } from "./romDruidMageInspirationRuntime"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; const EARTH_CHAIN_ID = "rom-druid-rom-mage-elite-15"; const earthChain = ROM_ALL_ABILITIES.find((ability) => ability.id === EARTH_CHAIN_ID); function inspirationAura(targetId: string) { return useCombatStore.getState().auras.find((aura) => aura.targetId === targetId && aura.definition.utilityTags?.includes(ROM_DRUID_MAGE_INSPIRATION_AURA_TAG)); } function priest(name: string, seed: string, index: number) { return createConfiguredPartyMember({ name, classId: "rom-priest", specialization: "Primary", role: "damage", }, 45, index, seed); } describe("CoA Druid/Mage Inspiration player and companion integration", () => { beforeEach(() => { resetPartyRuntimeClock(); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 45, role: "healer", }); usePartyStore.getState().initializeParty( "inspiration-party-test", 45, useCombatStore.getState().health, "healer", 4, "rom", ); }); it("awards one nearby-party stack for a player's completed Nature Power spend and applies cast haste", () => { if (!earthChain) throw new Error("Missing generated Druid/Mage Earth Chain row"); useCombatStore.getState().initializeCharacter({ classId: "rom-druid", secondaryClassId: "rom-mage", raceId: "rom-elf", level: 45, role: "damage", }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); const seed = "inspiration-player-spend"; const edge = priest("Range Edge", seed, 0); const outside = priest("Outside Range", seed, 1); const fallen = { ...priest("Fallen Ally", seed, 2), health: 0 }; usePartyStore.setState({ members: [edge, outside, fallen] }); registerPartyRuntimePosition(edge.id, [3, 4, 0]); registerPartyRuntimePosition(outside.id, [5.01, 0, 0]); registerPartyRuntimePosition(fallen.id, [1, 0, 0]); useCombatStore.getState().synchronizeActors(); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": 1 }, maxResourcePools: { ...state.maxResourcePools, "nature-power": 10 }, })); const targetId = "inspiration-player-target"; useCombatStore.getState().registerMob(targetId, { name: "Target", level: 1, maxHealth: 100_000, xpReward: 0 }); setMobPosition(targetId, [2, 0, 0]); useCombatStore.getState().selectMob(targetId); expect(useCombatStore.getState().castAbility(EARTH_CHAIN_ID, { origin: [0, 0, 0], targetActorId: targetId, now: 1_000, }).ok).toBe(true); const unbuffedCastDuration = useCombatStore.getState().activeCast?.originalDurationMs ?? 0; expect(unbuffedCastDuration).toBeGreaterThan(0); useCombatStore.getState().tick(2, 3_000); expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(0); expect(inspirationAura(PLAYER_AGGRO_ID)).toMatchObject({ stacks: 1, expiresAt: 13_000 }); expect(inspirationAura(edge.id)?.stacks).toBe(1); expect(inspirationAura(outside.id)).toBeUndefined(); expect(inspirationAura(fallen.id)).toBeUndefined(); expect(useCombatStore.getState().castAbility(EARTH_CHAIN_ID, { origin: [0, 0, 0], targetActorId: targetId, now: 7_000, }).ok).toBe(true); expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeLessThan(unbuffedCastDuration); }); it("awards one caster-centered stack when a generated Druid/Mage companion spends Nature Power", () => { if (!earthChain) throw new Error("Missing generated Druid/Mage Earth Chain row"); const seed = "inspiration-companion-spend"; const caster = createConfiguredPartyMember({ name: "Druid/Mage", classId: "rom-druid", secondaryClassId: "rom-mage", specialization: "Primary", role: "damage", }, 45, 0, seed); const outside = priest("Outside Range", seed, 1); usePartyStore.setState({ members: [{ ...caster, resourcePools: { ...caster.resourcePools, "nature-power": 1 }, activeCast: { abilityId: EARTH_CHAIN_ID, school: "nature", mode: "cast", startedAt: 0, completesAt: 1_000, originalDurationMs: 1_000, pushbackHits: 0, origin: [0, 0, 0], targetId: "inspiration-companion-target", }, activeCastTargetId: "inspiration-companion-target", status: "attacking", }, outside], command: "attack", activeMobId: "inspiration-companion-target", }); registerPartyRuntimePosition(caster.id, [0, 0, 0]); registerPartyRuntimePosition(outside.id, [5.01, 0, 0]); useCombatStore.getState().synchronizeActors(); useCombatStore.getState().registerMob("inspiration-companion-target", { name: "Target", level: 1, maxHealth: 100_000, xpReward: 0, }); setMobPosition("inspiration-companion-target", [2, 0, 0]); useCombatStore.getState().damageMob("inspiration-companion-target", 1, 500); advanceDungeonPartyCombat(1_000); expect(usePartyStore.getState().members[0]?.resourcePools["nature-power"]).toBe(0); expect(inspirationAura(caster.id)?.stacks).toBe(1); expect(inspirationAura(PLAYER_AGGRO_ID)?.stacks).toBe(1); expect(inspirationAura(outside.id)).toBeUndefined(); }); it("applies Inspiration spell haste only once to a Mage receiving the party buff", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-mage", secondaryClassId: "rom-druid", raceId: "rom-elf", level: 45, role: "damage", }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); const targetId = "inspiration-mage-target"; useCombatStore.getState().registerMob(targetId, { name: "Target", level: 1, maxHealth: 100_000, xpReward: 0 }); setMobPosition(targetId, [2, 0, 0]); useCombatStore.getState().selectMob(targetId); const ability = useCombatStore.getState().abilities.find((candidate) => candidate.id === "rom-mage-primary-1"); expect(ability?.castTimeMs).toBe(3_000); expect(useCombatStore.getState().castAbility("rom-mage-primary-1", { origin: [0, 0, 0], targetActorId: targetId, now: 1_000, }).ok).toBe(true); const unbuffedDuration = useCombatStore.getState().activeCast?.originalDurationMs ?? 0; expect(unbuffedDuration).toBeGreaterThan(0); useCombatStore.getState().tick(3, 4_000); useCombatStore.setState((state) => ({ auras: [...romDruidMageInspirationApplySpendStack( state.auras, "druid-mage-source", [PLAYER_AGGRO_ID], 5_000, )], })); expect(inspirationAura(PLAYER_AGGRO_ID)?.stacks).toBe(1); expect(romDruidMageInspirationHastePercent( useCombatStore.getState().auras, PLAYER_AGGRO_ID, 5_000, )).toBe(2); expect(useCombatStore.getState().castAbility("rom-mage-primary-1", { origin: [0, 0, 0], targetActorId: targetId, now: 5_000, }).ok).toBe(true); expect(useCombatStore.getState().activeCast?.startedAt).toBe(5_000); const buffedDuration = useCombatStore.getState().activeCast?.originalDurationMs ?? 0; const durationReduction = 1 - buffedDuration / unbuffedDuration; expect(durationReduction).toBeGreaterThan(0.01); expect(durationReduction).toBeLessThan(0.03); }); });