import { beforeAll, beforeEach, describe, expect, it } from "vitest"; import { abilityById, 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" | "rom-mage") { 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 }, })); } function prepareMageDruid() { useCombatStore.getState().initializeCharacter({ classId: "rom-mage", secondaryClassId: "rom-druid", raceId: "rom-elf", level: 80, }); useCombatStore.setState((state) => ({ health: 1, resource: 1_000, maxResource: 1_000, resourcePools: { ...state.resourcePools, mana: 1_000 }, maxResourcePools: { ...state.maxResourcePools, mana: 1_000 }, })); } describe("RuneWaker Druid runtime", () => { beforeAll(async () => { await preloadAbilityCatalogsForClasses(["rom-druid", "rom-mage"]); }); 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("converts exactly 10% MP into four Nature Power with Essence Extraction", () => { prepareDruid("rom-warden"); const essence = ability(494373); expect(essence.costs).toContainEqual({ resource: "mana", amount: 10, percentage: true }); expect(useCombatStore.getState().resourcePools).toMatchObject({ mana: 1_000, "nature-power": 0 }); expect(useCombatStore.getState().castAbility(essence.id, { now: 1_000 }).ok).toBe(true); expect(useCombatStore.getState().resourcePools.mana).toBe(900); expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(4); }); it("casts the current Druid/Rogue Necrotic Wound only with poison and Nature Power, then spreads three hits", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-druid", secondaryClassId: "rom-rogue", raceId: "rom-elf", level: 80, }); useCombatStore.setState((state) => ({ resource: 1_000, maxResource: 1_000, resourcePools: { ...state.resourcePools, mana: 1_000, energy: 100, "nature-power": 1 }, maxResourcePools: { ...state.maxResourcePools, mana: 1_000, energy: 100 }, cooldowns: { ...state.cooldowns, [ability(494023).id]: 11_000 }, })); const primary = "rom-druid-wound-primary"; const nearby = "rom-druid-wound-nearby"; const outside = "rom-druid-wound-outside"; for (const [id, position] of [ [primary, [4, 0, 0]], [nearby, [11, 0, 0]], [outside, [13, 0, 0]], ] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000_000, xpReward: 0 }); setMobPosition(id, position); useCombatStore.getState().damageMob(id, 1, 100); } const wound = ability(494321); const poison = ability(494024); const runtimePoison = abilityById(poison.id); expect(runtimePoison).toMatchObject({ id: poison.id, dbcSpellId: 494024, effects: expect.arrayContaining([expect.objectContaining({ kind: "dot", tag: "rom-druid-corrosive-poison", })]), }); expect(useCombatStore.getState().castAbility(wound.id, { targetActorId: primary, now: 1_000 })) .toMatchObject({ ok: false, reason: "condition-not-met" }); expect(useCombatStore.getState().castAbility(poison.id, { targetActorId: primary, now: 2_000 }).ok).toBe(true); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": 0 }, })); expect(useCombatStore.getState().castAbility(wound.id, { targetActorId: primary, now: 3_500 })) .toMatchObject({ ok: false, reason: "not-enough-resource" }); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": 1 }, })); expect(useCombatStore.getState().castAbility(wound.id, { targetActorId: primary, now: 5_000 }).ok).toBe(true); expect(useCombatStore.getState().resourcePools).toMatchObject({ mana: 960, "nature-power": 0, energy: 75 }); finishCast(5_000); const woundHits = useCombatStore.getState().combatEvents.filter((event) => ( event.abilityId === wound.id && event.kind === "damage" )); expect(woundHits).toHaveLength(6); const activePoisonTargets = useCombatStore.getState().effects.filter((effect) => ( effect.kind === "dot" && effect.tag === "rom-druid-corrosive-poison" && effect.remainingTicks === 12 )).map((effect) => effect.targetId).sort(); expect(activePoisonTargets).toEqual([nearby, primary].sort()); expect(useCombatStore.getState().mobs[outside]!.health).toBe(999_999); expect(useCombatStore.getState().cooldowns[ability(494023).id]).toBe(10_000); expect(useCombatStore.getState().mobs[primary]!.statuses.some((status) => status.kind === "root")).toBe(false); }); it("applies Grace of the Forest's Nature Power bonus only when a point is held", () => { prepareDruid("rom-warden"); const grace = ability(494937); expect(useCombatStore.getState().castAbility(grace.id, { now: 1_000 }).ok).toBe(true); const baseAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID); expect(baseAuras.some((aura) => aura.definition.modifiers?.some((modifier) => modifier.kind === "resource" && modifier.resource === "mana" && modifier.property === "maximum" && modifier.operation === "percent" && modifier.value === 0.1))).toBe(false); expect(baseAuras.some((aura) => aura.definition.modifiers?.some((modifier) => modifier.kind === "healing" && modifier.direction === "done" && modifier.operation === "percent" && modifier.value === 0.03))).toBe(true); prepareDruid("rom-warden"); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": 1 }, })); expect(useCombatStore.getState().castAbility(grace.id, { now: 2_000 }).ok).toBe(true); const poweredAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID); expect(poweredAuras.find((aura) => aura.definition.modifiers?.some((modifier) => modifier.kind === "resource" && modifier.resource === "mana" && modifier.property === "maximum" && modifier.operation === "percent" && modifier.value === 0.1))?.expiresAt).toBe(6_000); expect(poweredAuras.find((aura) => aura.definition.id === "rom-druid-grace-of-the-forest-powered-healing")?.expiresAt) .toBe(6_000); }); it("selects and applies the source Seed defense branch to its target", () => { const branches = [ { spellId: 493546, baseComponentId: 503815, poweredComponentId: 620458, stat: "magical-defense" }, { spellId: 493545, baseComponentId: 503814, poweredComponentId: 620457, stat: "armor" }, ] as const; branches.forEach(({ spellId, baseComponentId, poweredComponentId, stat }, index) => { for (const { naturePower, componentId } of [ { naturePower: 0, componentId: baseComponentId }, { naturePower: 1, componentId: poweredComponentId }, ]) { const targetId = `${TARGET_ID}-seed-${spellId}-${naturePower}`; prepareDruid(); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": naturePower }, })); useCombatStore.getState().registerMob(targetId, { name: "Seed Target", maxHealth: 10_000 }); setMobPosition(targetId, [5, 0, 0]); const seed = ability(spellId); const now = 2_000 + index * 4_000 + naturePower * 1_000; expect(useCombatStore.getState().castAbility(seed.id, { targetActorId: targetId, now }).ok).toBe(true); expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(1); const seedAura = useCombatStore.getState().auras.find((aura) => aura.targetId === targetId && aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === stat)); expect(seedAura).toMatchObject({ expiresAt: now + 20_000, definition: expect.objectContaining({ id: expect.stringContaining(`-${componentId}-`), modifiers: [expect.objectContaining({ kind: "stat", stat, operation: "percent", value: -0.02 })], }), }); } }); }); 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); }); it("suppresses the stale Green Guardian HoTs when the exact-pair active skill's protection expires", () => { prepareMageDruid(); const state = useCombatStore.getState(); const passive = ability(494331); const protection = ability(493533); expect(state.abilities.some((entry) => entry.id === passive.id && !entry.passive)).toBe(true); expect(passive.effects).toEqual([expect.objectContaining({ kind: "apply-aura" })]); const protectionCast = useCombatStore.getState().castAbility(protection.id, { targetActorId: null, now: 1_000, }); expect(protectionCast.ok).toBe(true); const taggedProtection = useCombatStore.getState().auras.find((aura) => ( aura.definition.utilityTags?.includes("rom-druid-mother-earth-protection") )); expect(taggedProtection?.expiresAt).toBe(6_000); const startingHealth = useCombatStore.getState().health; useCombatStore.getState().tick(0, 5_999); expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id)).toHaveLength(0); expect(useCombatStore.getState().health).toBe(startingHealth); useCombatStore.getState().tick(0, 6_000); expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id)).toHaveLength(0); expect(useCombatStore.getState().health).toBe(startingHealth); }); it("does not trigger Green Guardian for another pair or another aura caster", () => { prepareDruid("rom-mage"); const protection = ability(493533); const wrongPairPassive = useCombatStore.getState().abilities.find((entry) => entry.dbcSpellId === 494331); expect(wrongPairPassive).toBeUndefined(); expect(useCombatStore.getState().castAbility(protection.id, { targetActorId: null, now: 1_000, }).ok).toBe(true); useCombatStore.getState().tick(1, 6_000); expect(useCombatStore.getState().effects.filter((effect) => effect.kind === "hot")).toHaveLength(0); prepareMageDruid(); const passive = ability(494331); expect(useCombatStore.getState().castAbility(protection.id, { targetActorId: null, now: 10_000, }).ok).toBe(true); const taggedProtection = useCombatStore.getState().auras.find((aura) => ( aura.definition.utilityTags?.includes("rom-druid-mother-earth-protection") )); expect(taggedProtection).toBeDefined(); useCombatStore.setState((state) => ({ auras: state.auras.map((aura) => aura.instanceId === taggedProtection!.instanceId ? { ...aura, sourceId: "another-caster" } : aura), })); useCombatStore.getState().tick(1, taggedProtection!.expiresAt!); expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id)).toHaveLength(0); }); it("suppresses stale Green Guardian HoTs on a friendly protection recipient", () => { prepareMageDruid(); usePartyStore.getState().initializeParty("green-guardian-recipient", 80, 1, "damage", 5, "rom"); const recipient = usePartyStore.getState().members[0]!; usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === recipient.id ? { ...member, health: 1 } : member) })); registerPartyRuntimePosition(recipient.id, [4, 0, 0]); useCombatStore.getState().synchronizeActors(); const passive = ability(494331); const protection = ability(493533); expect(useCombatStore.getState().castAbility(protection.id, { targetActorId: recipient.id, now: 1_000, }).ok).toBe(true); const protectionAura = useCombatStore.getState().auras.find((aura) => ( aura.sourceId === PLAYER_AURA_ENTITY_ID && aura.targetId === recipient.id && aura.definition.utilityTags?.includes("rom-druid-mother-earth-protection") )); expect(protectionAura?.expiresAt).toBe(6_000); useCombatStore.getState().tick(0, protectionAura!.expiresAt!); expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id && effect.targetId === recipient.id)).toHaveLength(0); expect(usePartyStore.getState().members.find((member) => member.id === recipient.id)!.health).toBe(1); }); it("suppresses stale Green Guardian HoTs for a companion's protection expiry", () => { prepareMageDruid(); const caster = createConfiguredPartyMember({ name: "Mage Druid", classId: "rom-mage", secondaryClassId: "rom-druid", specialization: "Mage", specializationId: "rom-mage-druid", talentTreeId: "rom-mage-druid", role: "damage", }, 80, 0, "green-guardian-companion"); usePartyStore.getState().initializeParty("green-guardian-companion", 80, useCombatStore.getState().health, "damage", 2, "rom"); usePartyStore.setState({ members: [caster] }); useCombatStore.getState().synchronizeActors(); const passive = ability(494331); const protection = ability(493533); const protectionEffect = protection.effects.find((effect) => ( effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-druid-mother-earth-protection") )); if (!protectionEffect || protectionEffect.kind !== "apply-aura") { throw new Error("Mother Earth's Protection did not expose its tagged aura effect."); } expect(useCombatStore.getState().applyActorAura( caster.id, PLAYER_AURA_ENTITY_ID, protection, protectionEffect, 1_000, )).toBe(true); useCombatStore.getState().tick(0, 6_000); const triggered = useCombatStore.getState().effects.filter((effect) => ( effect.sourceAbilityId === passive.id && effect.sourceActorId === caster.id )); expect(passive.passive).toBe(false); expect(triggered).toHaveLength(0); }); });