import { beforeAll, beforeEach, describe, expect, it } from "vitest"; import { preloadAbilityCatalogsForClasses } from "./abilityCatalog"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore"; import { installCombatClock } from "./combatClock"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { companionResourceMaximum } from "./combatActorAuthority"; import { romDruidPeriodicHealAmount } from "./romDruidOverrides"; import { ROM_ALL_ABILITIES } from "./romAbilityCatalog"; const TARGET_ID = "rom-druid-runtime-target"; function ability(spellId: number) { const result = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === spellId); expect(result, `RoM Druid spell ${spellId}`).toBeDefined(); return result!; } function finishCast(now: number) { const cast = useCombatStore.getState().activeCast; if (cast) useCombatStore.getState().tick((cast.completesAt - now) / 1_000 + 0.01, cast.completesAt); } function prepareDruid(secondaryClassId?: "rom-warden" | "rom-scout") { useCombatStore.getState().initializeCharacter({ classId: "rom-druid", ...(secondaryClassId ? { secondaryClassId } : {}), raceId: "rom-elf", level: 80, }); useCombatStore.setState((state) => ({ resource: 1_000, maxResource: 1_000, resourcePools: { ...state.resourcePools, mana: 1_000, "nature-power": 0 }, maxResourcePools: { ...state.maxResourcePools, mana: 1_000 }, })); } describe("RuneWaker Druid runtime", () => { beforeAll(async () => { await preloadAbilityCatalogsForClasses(["rom-druid"]); }); beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.setState({ playerPosition: [0, 0, 0] }); }); it("casts Spirit Guidance for one Nature Power and applies the source three-second Charm", () => { prepareDruid(); useCombatStore.getState().registerMob(TARGET_ID, { name: "Guidance Target", maxHealth: 10_000 }); setMobPosition(TARGET_ID, [5, 0, 0]); const spiritGuidance = ability(493536); expect(spiritGuidance.costs).toContainEqual({ resource: "nature-power", amount: 1 }); expect(useCombatStore.getState().castAbility(spiritGuidance.id, { targetActorId: TARGET_ID, now: 1_000 })) .toMatchObject({ ok: false, reason: "not-enough-resource" }); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": 1 } })); expect(useCombatStore.getState().castAbility(spiritGuidance.id, { targetActorId: TARGET_ID, now: 3_000 }).ok).toBe(true); const activeCast = useCombatStore.getState().activeCast; expect(activeCast?.abilityId).toBe(spiritGuidance.id); finishCast(3_000); expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(0); expect(useCombatStore.getState().mobs[TARGET_ID]!.statuses).toContainEqual(expect.objectContaining({ kind: "charm", endsAt: activeCast!.completesAt + 3_000, })); // The combat model supports the Charm control effect; it has no faction/allegiance conversion state. }); it("applies Unity with Mother Earth as a 15-second healing and guaranteed spell-crit aura", () => { prepareDruid(); const unity = ability(493548); expect(unity.target).toBe("self"); expect(useCombatStore.getState().castAbility(unity.id, { now: 1_000 }).ok).toBe(true); const aura = useCombatStore.getState().auras.find((entry) => ( entry.targetId === PLAYER_AURA_ENTITY_ID && entry.definition.utilityTags?.includes("rom-druid-unity-with-mother-earth") )); expect(aura?.expiresAt).toBe(16_000); expect(aura?.definition.modifiers).toContainEqual({ kind: "stat", stat: "magical-critical-strike", operation: "flat", value: 100, }); expect(aura?.definition.modifiers).toContainEqual({ kind: "healing", direction: "done", operation: "percent", value: 0.1, }); useCombatStore.getState().registerMob(TARGET_ID, { name: "Unity Target", maxHealth: 10_000 }); setMobPosition(TARGET_ID, [5, 0, 0]); const arrow = ability(493538); expect(useCombatStore.getState().castAbility(arrow.id, { targetActorId: TARGET_ID, now: 3_000 }).ok).toBe(true); finishCast(3_000); expect(useCombatStore.getState().lastCombatResult?.critical).toBe(true); }); it("prevents movement during Unity and restores movement when its 15-second aura expires", () => { let now = 1_000; const restoreClock = installCombatClock(() => now); try { prepareDruid(); const unity = ability(493548); expect(useCombatStore.getState().castAbility(unity.id, { now }).ok).toBe(true); useCombatStore.getState().setPlayerPosition([6, 0, 0]); expect(useCombatStore.getState().playerPosition).toEqual([0, 0, 0]); now = 16_001; useCombatStore.getState().setPlayerPosition([6, 0, 0]); expect(useCombatStore.getState().playerPosition).toEqual([6, 0, 0]); } finally { restoreClock(); } }); it("raises Nature Power's maximum to 15 only after learning Brilliance of Nature", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-druid", secondaryClassId: "rom-scout", raceId: "rom-elf", level: 44, }); expect(useCombatStore.getState().maxResourcePools["nature-power"]).toBe(10); useCombatStore.getState().initializeCharacter({ classId: "rom-druid", secondaryClassId: "rom-scout", raceId: "rom-elf", level: 45, }); expect(useCombatStore.getState().abilities.some((entry) => entry.dbcSpellId === 494931 && entry.passive)).toBe(true); expect(useCombatStore.getState().maxResourcePools["nature-power"]).toBe(15); const companion = createConfiguredPartyMember({ name: "Druid Companion", classId: "rom-druid", secondaryClassId: "rom-scout", raceId: "rom-elf", specialization: "Primary", role: "healer", }, 45, 0, "rom-druid-brilliance-cap"); expect(companionResourceMaximum(companion, "nature-power")).toBe(15); }); it("adds exactly one percent of each healing target's max health to the Spirit of Life tick", () => { expect(romDruidPeriodicHealAmount(500, 10_000, 0.01)).toBe(600); expect(romDruidPeriodicHealAmount(500, 5_000, 0.01)).toBe(550); }); it("gives Spirit of Life a 40-second, four-second party heal within its source radius", () => { prepareDruid("rom-warden"); usePartyStore.getState().initializeParty("rom-druid-spirit-of-life", 80, useCombatStore.getState().health, "healer", 5, "rom"); const [near, far] = usePartyStore.getState().members; expect(near).toBeDefined(); expect(far).toBeDefined(); useCombatStore.setState({ health: 1 }); usePartyStore.setState((state) => ({ members: state.members.map((member) => ({ ...member, health: 1 })) })); registerPartyRuntimePosition(near!.id, [5, 0, 0]); registerPartyRuntimePosition(far!.id, [12, 0, 0]); useCombatStore.getState().synchronizeActors(); const spirit = ability(494938); const hot = spirit.effects.find((effect) => effect.kind === "hot"); expect(hot).toMatchObject({ ticks: 10, intervalMs: 4_000, scope: "party", radius: 7, center: "caster", percentMaxHealth: 0.01, }); expect(spirit.effects.some((effect) => effect.kind === "summon" || effect.kind === "heal")).toBe(false); const beforePlayer = useCombatStore.getState().health; const beforeNear = usePartyStore.getState().members.find((member) => member.id === near!.id)!.health; const beforeFar = usePartyStore.getState().members.find((member) => member.id === far!.id)!.health; expect(useCombatStore.getState().castAbility(spirit.id, { now: 1_000 }).ok).toBe(true); expect(useCombatStore.getState().summons.some((summon) => summon.sourceAbilityId === spirit.id)).toBe(false); useCombatStore.getState().tick(4.1, 5_100); expect(useCombatStore.getState().health).toBeGreaterThan(beforePlayer); expect(usePartyStore.getState().members.find((member) => member.id === near!.id)!.health).toBeGreaterThan(beforeNear); expect(usePartyStore.getState().members.find((member) => member.id === far!.id)!.health).toBe(beforeFar); }); it("casts Savage Blessing on the selected friendly target with beneficial hit buffs", () => { prepareDruid(); usePartyStore.getState().initializeParty("rom-druid-savage-blessing", 80, useCombatStore.getState().health, "healer", 5, "rom"); const ally = usePartyStore.getState().members[0]!; registerPartyRuntimePosition(ally.id, [4, 0, 0]); useCombatStore.getState().synchronizeActors(); const blessing = ability(493531); expect(blessing.target).toBe("friendly"); expect(useCombatStore.getState().castAbility(blessing.id, { targetActorId: ally.id, now: 1_000 }).ok).toBe(true); finishCast(1_000); const buffs = useCombatStore.getState().auras.filter((aura) => aura.definition.name.startsWith("Savage Blessing")); expect(buffs.length).toBeGreaterThanOrEqual(2); expect(buffs.every((aura) => aura.targetId === ally.id && aura.definition.disposition === "buff")).toBe(true); }); });