import { describe, expect, it } from "vitest"; import { abilitiesForClass } from "./abilityCatalog"; import { targetingRulesForAbility } from "./abilityRules"; import { coaPeriodicActionMaintainedOnTarget, usesCoaTargetedPeriodicMaintenance, } from "./coaPeriodicRotationRuntime"; import { evaluateCombatRotation, type RotationContext } from "./combatRotation"; function coaPeriodicAbility(classId: "ranger" | "witch-doctor", name: string) { const ability = abilitiesForClass(classId).find((candidate) => candidate.name === name && candidate.effects.some((effect) => effect.kind === "dot" || effect.kind === "hot")); if (!ability) throw new Error(`Missing CoA ${classId} periodic ability ${name}.`); return ability; } function contextFor( classId: RotationContext["classId"], ability: RotationContext["abilities"][number], options: { readonly selfId?: string; readonly targetId?: string; readonly targetHealth?: number; readonly maxTargetHealth?: number; readonly friendlyTargets?: RotationContext["friendlyTargets"]; readonly maintainedPeriodicEffects?: RotationContext["maintainedPeriodicEffects"]; } = {}, ): RotationContext { const primaryResource = classId === "ranger" ? "focus" : classId === "barbarian" ? "energy" : "mana"; return { now: 5_000, classId, specializationId: classId === "ranger" ? "ranger:archery" : "witch-doctor:brewing", role: classId === "witch-doctor" ? "healer" : "damage", level: 60, health: 1_000, maxHealth: 1_000, selfId: options.selfId ?? "coa-periodic-caster", resources: { [primaryResource]: 100 }, maximumResources: { [primaryResource]: 100 }, primaryResource, comboPoints: 0, comboTargetId: null, runes: [], abilities: [ability], hostileTarget: options.targetId && ability.target !== "friendly" ? { id: options.targetId, health: options.targetHealth ?? 800, maxHealth: options.maxTargetHealth ?? 1_000 } : null, friendlyTargets: options.friendlyTargets, maintainedAbilityIds: new Set([ability.id]), maintainedPeriodicEffects: options.maintainedPeriodicEffects, }; } describe("target-specific CoA periodic maintenance", () => { it("keeps a live Ranger DoT on enemy A from blocking the same cast on enemy B", () => { const serratedShot = coaPeriodicAbility("ranger", "Serrated Shot"); expect(serratedShot.source).toMatch(/^coa-/); expect(serratedShot.target).toBe("hostile"); expect(targetingRulesForAbility(serratedShot).shape).toBe("unit"); const onA = contextFor("ranger", serratedShot, { targetId: "enemy-b", maintainedPeriodicEffects: [{ abilityId: serratedShot.id, sourceActorId: "coa-periodic-caster", targetId: "enemy-a", remainingMs: 2_000, }], }); expect(onA.maintainedAbilityIds?.has(serratedShot.id)).toBe(true); expect(usesCoaTargetedPeriodicMaintenance(serratedShot, onA)).toBe(true); expect(coaPeriodicActionMaintainedOnTarget(serratedShot, onA, "enemy-b")).toBe(false); expect(evaluateCombatRotation(onA)).toMatchObject({ ability: { id: serratedShot.id }, targetId: "enemy-b" }); const onB = contextFor("ranger", serratedShot, { targetId: "enemy-b", maintainedPeriodicEffects: [{ abilityId: serratedShot.id, sourceActorId: "coa-periodic-caster", targetId: "enemy-b", remainingMs: 2_000, }], }); expect(coaPeriodicActionMaintainedOnTarget(serratedShot, onB, "enemy-b")).toBe(true); expect(evaluateCombatRotation(onB)).toBeNull(); }); it("uses the intended friendly recipient and ignores foreign or expired HoTs", () => { const mojoBeam = coaPeriodicAbility("witch-doctor", "Mojo Beam"); expect(mojoBeam.source).toMatch(/^coa-/); expect(mojoBeam.target).toBe("friendly"); expect(targetingRulesForAbility(mojoBeam).shape).toBe("unit"); const casterOnlyHot = { ...mojoBeam, effects: [{ kind: "hot" as const, coefficient: 1, ticks: 2, intervalMs: 1, recipient: "caster" as const }], }; expect(usesCoaTargetedPeriodicMaintenance(casterOnlyHot)).toBe(false); const allies = [ { id: "ally-a", health: 700, maxHealth: 1_000, role: "tank" as const }, { id: "ally-b", health: 400, maxHealth: 1_000, role: "damage" as const }, ]; const onAllyA = contextFor("witch-doctor", mojoBeam, { friendlyTargets: allies, maintainedPeriodicEffects: [{ abilityId: mojoBeam.id, sourceActorId: "coa-periodic-caster", targetId: "ally-a", remainingMs: 2_000, }], }); expect(onAllyA.friendlyTargets?.[1]?.id).toBe("ally-b"); expect(coaPeriodicActionMaintainedOnTarget(mojoBeam, onAllyA, "ally-b")).toBe(false); expect(evaluateCombatRotation(onAllyA)).toMatchObject({ ability: { id: mojoBeam.id }, targetId: "ally-b" }); const foreign = contextFor("witch-doctor", mojoBeam, { friendlyTargets: allies, maintainedPeriodicEffects: [{ abilityId: mojoBeam.id, sourceActorId: "another-witch-doctor", targetId: "ally-b", remainingMs: 2_000, }], }); expect(usesCoaTargetedPeriodicMaintenance(mojoBeam, foreign)).toBe(true); expect(coaPeriodicActionMaintainedOnTarget(mojoBeam, foreign, "ally-b")).toBe(false); expect(evaluateCombatRotation(foreign)).toMatchObject({ ability: { id: mojoBeam.id }, targetId: "ally-b" }); const onRecipient = contextFor("witch-doctor", mojoBeam, { friendlyTargets: allies, maintainedPeriodicEffects: [{ abilityId: mojoBeam.id, sourceActorId: "coa-periodic-caster", targetId: "ally-b", remainingMs: 2_000, }], }); expect(evaluateCombatRotation(onRecipient)).toBeNull(); const expired = contextFor("witch-doctor", mojoBeam, { friendlyTargets: allies, maintainedPeriodicEffects: [{ abilityId: mojoBeam.id, sourceActorId: "coa-periodic-caster", targetId: "ally-b", remainingMs: 0, }], }); expect(coaPeriodicActionMaintainedOnTarget(mojoBeam, expired, "ally-b")).toBe(false); expect(evaluateCombatRotation(expired)).toMatchObject({ ability: { id: mojoBeam.id }, targetId: "ally-b" }); }); it("leaves non-CoA and area periodic abilities on their existing maintenance path", () => { const moonfire = abilitiesForClass("druid").find((ability) => ability.name === "Moonfire"); if (!moonfire) throw new Error("Missing WoW Moonfire."); const wow = contextFor("druid", moonfire, { targetId: "enemy-b" }); const wowMaintenance = { ...wow, specializationId: "druid:balance", maintainedPeriodicEffects: undefined, } satisfies RotationContext; expect(usesCoaTargetedPeriodicMaintenance(moonfire, wowMaintenance)).toBe(false); expect(evaluateCombatRotation({ ...wowMaintenance, maintainedAbilityIds: undefined })?.ability.id).toBe(moonfire.id); expect(evaluateCombatRotation({ ...wowMaintenance, maintainedAbilityIds: new Set([moonfire.id]), })).toBeNull(); const areaHot = abilitiesForClass("witch-doctor").find((ability) => ability.name === "Splash Potion"); if (!areaHot) throw new Error("Missing CoA area periodic action Splash Potion."); const areaContext = contextFor("witch-doctor", areaHot, { targetId: "enemy-b" }); expect(targetingRulesForAbility(areaHot).shape).not.toBe("unit"); expect(usesCoaTargetedPeriodicMaintenance(areaHot, areaContext)).toBe(false); }); });