import { describe, expect, it } from "vitest"; import { abilityById, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog"; import { ROM_ALL_ABILITIES } from "./romAbilityCatalog"; import { romAbilityNeedsRangedWeapon, romAbilityNeedsShield, romAbilityNeedsTwoHand, romAbilityRequiredTargetStateTag, romEffectsForCast, romTargetBleedCount, romTargetHasSlashBleed, romWarriorAbilityAdjustment, romWarriorThreatMultiplier, type RomCastMechanics } from "./romCombatMechanics"; const empty: RomCastMechanics = { sealStacks: 0, bleedCount: 0, charged: false, mainHandDagger: false, casterMaxHealth: 2_000, targetHealthPercent: 1, }; function cast(id: string, overrides: Partial = {}): readonly AbilityEffect[] { const ability = abilityById(id); expect(ability).toBeDefined(); return romEffectsForCast(ability!, { ...empty, ...overrides }); } function kinds(effects: readonly AbilityEffect[]): readonly string[] { return effects.map((effect) => effect.kind); } describe("RoM primary class conditional combat effects", () => { it("applies learned Priest/Warrior Condensed Rage only to physical damage", () => { const source = ROM_ALL_ABILITIES.find((ability) => ability.classId === "rom-priest")!; const physicalAbility: AbilityDefinition = { ...source, effects: [{ kind: "damage", coefficient: 0, basePoints: 100, school: "physical" }], }; const holyAbility: AbilityDefinition = { ...physicalAbility, effects: [{ kind: "damage", coefficient: 0, basePoints: 100, school: "holy" }], }; const otherClassAbility: AbilityDefinition = { ...physicalAbility, classId: "rom-warrior" }; const withPassive = { ...empty, learnedPassiveSpellIds: [493038] }; const withoutPassive = { ...empty, learnedPassiveSpellIds: [] }; expect(romEffectsForCast(physicalAbility, withPassive)[0]).toMatchObject({ basePoints: 122, school: "physical" }); expect(romEffectsForCast(physicalAbility, withoutPassive)[0]).toMatchObject({ basePoints: 100 }); expect(romEffectsForCast(holyAbility, withPassive)[0]).toMatchObject({ basePoints: 100, school: "holy" }); expect(romEffectsForCast(otherClassAbility, withPassive)[0]).toMatchObject({ basePoints: 100 }); }); it("recognizes source-required shields and ranged weapons across primary and elite rows", () => { const sourceAbility = (spellId: number) => ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === spellId)!; for (const spellId of [491333, 491484]) expect(romAbilityNeedsShield(sourceAbility(spellId))).toBe(true); for (const spellId of [490438, 491452, 491616, 494021, 494037, 494039, 494040, 494042, 494629]) { expect(romAbilityNeedsRangedWeapon(sourceAbility(spellId))).toBe(true); } }); it("selects the Nature's Power Healing Arrows HoT and consumes one point only when available", () => { const baseline = cast("rom-druid-rom-scout-elite-25", { naturePower: 0 }); const empowered = cast("rom-druid-rom-scout-elite-25", { naturePower: 1 }); expect(baseline).toHaveLength(1); expect(baseline[0]?.kind).toBe("hot"); if (baseline[0]?.kind === "hot") expect(baseline[0].basePoints).toBeCloseTo(18.88); expect(empowered).toHaveLength(2); expect(empowered[0]?.kind).toBe("hot"); if (empowered[0]?.kind === "hot") expect(empowered[0].basePoints).toBeCloseTo(23.6); expect(empowered[1]).toEqual({ kind: "resource-drain", amount: 1, burn: false, resource: "nature-power", recipient: "caster" }); expect(cast("rom-druid-rom-scout-elite-25", { naturePower: 0.5 }).filter((effect) => effect.kind === "resource-drain")) .toEqual([]); }); it("counts only active Rogue bleeds applied by the caster to that target", () => { const timed = [ { kind: "dot", sourceAbilityId: "rom-rogue-primary-1", sourceActorId: "rogue", targetId: "mob", remainingTicks: 3 }, { kind: "dot", sourceAbilityId: "rom-rogue-general-0", sourceActorId: "rogue", targetId: "mob", remainingTicks: 2 }, { kind: "dot", sourceAbilityId: "rom-rogue-general-0", sourceActorId: "rogue", targetId: "mob", remainingTicks: 1 }, { kind: "dot", sourceAbilityId: "rom-rogue-primary-9", sourceActorId: "other", targetId: "mob", remainingTicks: 2 }, { kind: "dot", sourceAbilityId: "rom-rogue-primary-4", sourceActorId: "rogue", targetId: "other-mob", remainingTicks: 2 }, ]; expect(romTargetBleedCount(timed, "rogue", "mob")).toBe(2); }); it("gives Wound Attack one hit plus one hit per active bleed, without inventing a DoT", () => { expect(kinds(cast("rom-rogue-primary-2"))).toEqual(["damage"]); expect(kinds(cast("rom-rogue-primary-2", { bleedCount: 1 }))).toEqual(["damage", "damage"]); expect(kinds(cast("rom-rogue-primary-2", { bleedCount: 2 }))).toEqual(["damage", "damage", "damage"]); expect(kinds(cast("rom-rogue-primary-1"))).toEqual(["damage"]); expect(kinds(cast("rom-rogue-primary-1", { bleedCount: 1 }))).toEqual(["damage", "dot"]); }); it("selects exactly one Electric Explosion hit for normal and Charged casts", () => { const normal = cast("rom-mage-primary-54"); const charged = cast("rom-mage-primary-54", { charged: true }); expect(kinds(normal)).toEqual(["damage"]); expect(kinds(charged)).toEqual(["damage"]); expect((charged[0] as Extract).basePoints) .toBeGreaterThan((normal[0] as Extract).basePoints ?? 0); }); it("requires Holy Seals for Punishment and scales its payoff by stack count", () => { expect(cast("rom-knight-general-0")).toEqual([]); const one = cast("rom-knight-general-0", { sealStacks: 1 }); const four = cast("rom-knight-general-0", { sealStacks: 4 }); expect(kinds(one)).toEqual(["damage", "combo"]); expect((four[0] as Extract).basePoints) .toBe(4 * ((one[0] as Extract).basePoints ?? 0)); }); it("gates Knight seal riders and grants Shield Bash's absorb to the caster", () => { expect(kinds(cast("rom-knight-primary-7"))).toEqual(["damage", "apply-aura"]); expect(kinds(cast("rom-knight-primary-7", { sealStacks: 4 })).filter((kind) => kind === "damage")).toHaveLength(2); expect(kinds(cast("rom-knight-primary-12"))).toEqual(["damage", "apply-aura"]); expect(kinds(cast("rom-knight-primary-12", { sealStacks: 4 })).filter((kind) => kind === "damage")).toHaveLength(2); const bash = cast("rom-knight-primary-11", { sealStacks: 3 }); const shield = bash.find((effect) => effect.kind === "apply-aura"); expect(shield?.kind === "apply-aura" && shield.recipient).toBe("caster"); expect(shield?.kind === "apply-aura" && shield.aura.absorbs?.[0]?.amount).toBe(300); expect(kinds(bash)).toEqual(["damage", "apply-aura", "combo"]); }); it("requires a dagger for Shadowstab and Sneak Attack bleeds", () => { expect(kinds(cast("rom-rogue-general-0"))).toEqual(["damage"]); expect(kinds(cast("rom-rogue-general-0", { mainHandDagger: true }))).toEqual(["damage", "dot"]); expect(kinds(cast("rom-rogue-primary-9"))).toEqual(["damage", "apply-aura"]); expect(kinds(cast("rom-rogue-primary-9", { mainHandDagger: true }))).toEqual(["damage", "apply-aura", "dot"]); }); it("requires an active Vulnerable target for Keen Attack", () => { const keenAttackId = "rom-warrior-rom-rogue-elite-35"; expect(cast(keenAttackId)).toEqual([]); expect(kinds(cast(keenAttackId, { targetTags: ["rom-vulnerable"] }))).toContain("damage"); }); it("gates Open Flank on Vulnerable and Opportunity on the resulting Weakened state", () => { const sourceAbility = (spellId: number) => ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === spellId)!; expect(romAbilityRequiredTargetStateTag(sourceAbility(491136))).toBe("rom-vulnerable"); expect(romAbilityRequiredTargetStateTag(sourceAbility(492617))).toBe("rom-weakened"); expect(cast("rom-warrior-primary-2")).toEqual([]); const openFlank = cast("rom-warrior-primary-2", { targetTags: ["rom-vulnerable"] }); expect(openFlank.some((effect) => effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-weakened"))).toBe(true); expect(cast("rom-warrior-rom-priest-elite-35")).toEqual([]); expect(kinds(cast("rom-warrior-rom-priest-elite-35", { targetTags: ["rom-weakened"] }))) .toContain("damage"); }); it("only applies Tactical Attack's Slash bleed when a Slash bleed is active", () => { expect(kinds(cast("rom-warrior-primary-3"))).toEqual(["damage"]); expect(kinds(cast("rom-warrior-primary-3", { slashBleed: true }))).toEqual(["damage", "dot"]); const effects = [ { kind: "dot", sourceAbilityId: "rom-warrior-general-0", sourceActorId: "warrior", targetId: "target", remainingTicks: 1 }, { kind: "dot", sourceAbilityId: "rom-warrior-general-0", sourceActorId: "warrior", targetId: "other", remainingTicks: 3 }, { kind: "dot", sourceAbilityId: "rom-warrior-general-0", sourceActorId: "warrior", targetId: "target", remainingTicks: 0 }, { kind: "dot", sourceAbilityId: "rom-rogue-primary-1", sourceActorId: "warrior", targetId: "target", remainingTicks: 3 }, ]; expect(romTargetHasSlashBleed(effects, "target")).toBe(true); expect(romTargetHasSlashBleed(effects.slice(1), "target")).toBe(false); }); it("applies explicit Warrior passive Rage and cooldown reductions to their source skills", () => { const sourceAbility = (spellId: number) => ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === spellId)!; expect(romAbilityNeedsTwoHand(sourceAbility(491142))).toBe(true); expect(romWarriorAbilityAdjustment(sourceAbility(490121), [491458])) .toEqual({ rageCostReduction: 10, cooldownReductionMs: 20_000 }); expect(romWarriorAbilityAdjustment(sourceAbility(490133), [493248])) .toEqual({ rageCostReduction: 20, cooldownReductionMs: 10_000 }); expect(romWarriorAbilityAdjustment(sourceAbility(490133), [499491])) .toEqual({ rageCostReduction: 25, cooldownReductionMs: 15_000 }); expect(romWarriorAbilityAdjustment(sourceAbility(490133), [494377])) .toEqual({ rageCostReduction: 0, cooldownReductionMs: 0 }); expect(romWarriorAbilityAdjustment(sourceAbility(490121), [])).toEqual({ rageCostReduction: 0, cooldownReductionMs: 0, }); expect(romWarriorAbilityAdjustment(sourceAbility(490053), [499827])) .toEqual({ rageCostReduction: 10, cooldownReductionMs: 0 }); }); it("removes only the four retired Holy Seal DoTs and applies CoA Probing Attack's conditional weapon-DPS value", () => { for (const spellId of [490148, 490149, 490150, 490151, 499909]) { const ability = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === spellId)!; const resolved = romEffectsForCast(ability, spellId === 490151 || spellId === 499909 ? { ...empty, sealStacks: 4 } : empty); expect(resolved.some((effect) => effect.kind === "dot")).toBe(false); expect(resolved.some((effect) => effect.kind === "damage")).toBe(true); if (spellId === 490148 || spellId === 490150) { expect(resolved.some((effect) => effect.kind === "combo")).toBe(true); } } const punishment = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === 499909)!; const atTwoSeals = romEffectsForCast(punishment, { ...empty, sealStacks: 2 }) .find((effect) => effect.kind === "damage"); const atFourSeals = romEffectsForCast(punishment, { ...empty, sealStacks: 4 }) .find((effect) => effect.kind === "damage"); expect(atTwoSeals?.kind).toBe("damage"); expect(atFourSeals?.kind).toBe("damage"); if (atTwoSeals?.kind === "damage" && atFourSeals?.kind === "damage") { expect(atTwoSeals.coefficient).toBeCloseTo(atFourSeals.coefficient / 2); } expect(romEffectsForCast(punishment, { ...empty, sealStacks: 0 })).toEqual([]); const probing = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === 493283)!; const base = romEffectsForCast(probing, empty).find((effect) => effect.kind === "damage"); const bleedEmpowered = romEffectsForCast(probing, { ...empty, bleedCount: 1 }) .find((effect) => effect.kind === "damage"); const hammerEmpowered = romEffectsForCast(probing, { ...empty, mainHandTwoHandHammer: true }) .find((effect) => effect.kind === "damage"); expect(base).toMatchObject({ kind: "damage", weaponDpsCoefficient: 6.643 }); expect(bleedEmpowered).toMatchObject({ kind: "damage", weaponDpsCoefficient: 11.169 }); expect(hammerEmpowered).toMatchObject({ kind: "damage", weaponDpsCoefficient: 11.169 }); }); it("applies Warrior/Warlock's physical-damage passive and Heroic Surprise Attack's on-hit payload", () => { const sourceAbility = (spellId: number) => ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === spellId)!; const rageBlow = sourceAbility(498706); const baseDamage = romEffectsForCast(rageBlow, empty).find((effect) => effect.kind === "damage"); const trainedDamage = romEffectsForCast(rageBlow, { ...empty, learnedPassiveSpellIds: [499806], }).find((effect) => effect.kind === "damage"); expect(baseDamage?.kind).toBe("damage"); expect(trainedDamage?.kind).toBe("damage"); if (baseDamage?.kind === "damage" && trainedDamage?.kind === "damage") { expect(baseDamage.school).toBe("physical"); expect(trainedDamage.coefficient).toBeCloseTo(baseDamage.coefficient * 1.2); } const surpriseAttack = sourceAbility(490129); const heroicEffects = romEffectsForCast(surpriseAttack, { ...empty, learnedPassiveSpellIds: [492661], }); const heroicHit = heroicEffects.find((effect) => effect.kind === "damage"); expect(heroicHit?.kind).toBe("damage"); if (heroicHit?.kind === "damage") { expect(heroicHit.onDamageResource).toEqual({ resource: "rage", amount: 25 }); } expect(romWarriorThreatMultiplier(surpriseAttack, [492661])).toBe(5); expect(romWarriorThreatMultiplier(surpriseAttack, [])).toBe(1); }); it("keeps auxiliary source rows from adding extra hits, heals, or false control", () => { expect(kinds(cast("rom-priest-general-2"))).toEqual(["hot"]); expect(kinds(cast("rom-mage-primary-1"))).toEqual(["damage"]); expect(kinds(cast("rom-mage-general-0"))).toEqual(["damage"]); expect(kinds(cast("rom-rogue-primary-0"))).toEqual(["apply-aura", "apply-aura"]); expect(kinds(cast("rom-scout-general-3"))).toEqual(["damage", "apply-aura"]); }); it("executes the source count for multi-hit Mage storms", () => { expect(kinds(cast("rom-mage-primary-5"))).toEqual(Array(5).fill("damage")); expect(kinds(cast("rom-mage-primary-12"))).toEqual([ "damage", "damage", "damage", "apply-aura", "apply-aura", ]); }); it("executes Moon Cleave's three source-authored attacks", () => { const effects = cast("rom-warrior-primary-8"); expect(effects.filter((effect) => effect.kind === "damage")).toHaveLength(3); expect(effects.filter((effect) => effect.kind === "scripted")).toHaveLength(1); }); it("only adds The Final Battle's second hit below 30% target health", () => { const atThreshold = cast("rom-warrior-rom-scout-elite-50", { targetHealthPercent: 0.3 }); const belowThreshold = cast("rom-warrior-rom-scout-elite-50", { targetHealthPercent: 0.29 }); expect(atThreshold.filter((effect) => effect.kind === "damage")).toHaveLength(1); expect(belowThreshold.filter((effect) => effect.kind === "damage")).toHaveLength(2); }); it("holds Shield of Discipline for 30 seconds and reflects only on being hit", () => { const effects = cast("rom-knight-general-7"); expect(kinds(effects)).toEqual(["apply-aura", "apply-aura", "apply-aura"]); const proc = effects.find((effect) => effect.kind === "apply-aura" && effect.aura.procs?.length); expect(proc?.kind === "apply-aura" && proc.aura.durationMs).toBe(30_000); expect(proc?.kind === "apply-aura" && proc.aura.procs?.[0]?.action).toMatchObject({ kind: "custom", id: "rom-shield-discipline-reflect", }); }); it("uses one breakable control effect for each single-target restraint", () => { for (const id of ["rom-scout-primary-4", "rom-mage-primary-56"]) { const effects = cast(id); expect(effects).toHaveLength(1); expect(effects[0]).toMatchObject({ kind: "status", maxTargets: 1, breakOnDamage: true }); } expect(kinds(cast("rom-rogue-primary-5"))).toEqual(["apply-aura"]); }); });