import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, abilityById, learnedAbilityForBuild } from "./abilityCatalog"; import { PLAYER_AGGRO_ID } from "./aggro"; import { applyAura as applyCombatAura } from "./combatAuras"; import { abilityDamageSchool, useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities"; import { ROM_MAGE_WARLOCK_PASSIVE_TAGS, ROM_BREATH_ERASE_THREAT_TAG, ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS, romMageWarlockCastEffects, romMageWarlockCriticalEffects, romFireLightningBurstReset, romMageWarlockThreatScale, } from "./romMageWarlockOverrides"; const ELITES = [ [15, 498744, "Static Resonance"], [20, 498745, "Industry Fire"], [25, 498748, "Lightning Display"], [30, 498750, "Breath Erase"], [35, 498751, "Elements of Pride"], [40, 498757, "Soul Stepping"], [45, 498758, "Deep Inspiration"], [50, 498759, "Fire Lightning Burst"], [60, 499813, "Soul Speed"], [70, 499814, "Elemental Extraction"], ] as const; function elite(level: number) { const result = abilityById(`rom-mage-rom-warlock-elite-${level}`); expect(result, `Mage/Warlock elite ${level}`).toBeDefined(); return result!; } function initialize(level: number): void { useCombatStore.getState().initializeCharacter({ classId: "rom-mage", secondaryClassId: "rom-warlock", raceId: "rom-dwarf", level, }); } function registerTarget(id = "mage-warlock-dummy", health = 100_000): void { useCombatStore.getState().registerMob(id, { name: id, maxHealth: health }); setMobPosition(id, [3, 0, 0]); useCombatStore.getState().selectMob(id); } describe("accepted RoM Mage/Warlock dwarf pair", () => { beforeEach(() => { clearMobRuntimeRegistry(); useCombatStore.setState({ playerPosition: [0, 0, 0] }); }); it.each([10, 30, 60, 79])("learns the correct primary, secondary, and elite kit at level %i", (level) => { const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === "rom-mage")!; expect(identity).toMatchObject({ raceId: "rom-dwarf", secondaryClassId: "rom-warlock", role: "damage" }); const learned = abilitiesForCharacter(identity.classId, identity.secondaryClassId) .filter((ability) => learnedAbilityForBuild(ability, level, {}) !== null); expect(learned.some((ability) => ability.id === "rom-mage-primary-1")).toBe(true); expect(learned.some((ability) => ability.id === "rom-warlock-general-0")).toBe(true); expect(learned.filter((ability) => ability.secondaryClassId === "rom-warlock") .map((ability) => [ability.unlockLevel, ability.dbcSpellId, ability.name])) .toEqual(ELITES.filter(([unlock]) => unlock <= level)); }); it("preserves source multi-hit and area damage, defensive absorb, control, and threat tools", () => { expect(abilityById("rom-mage-primary-5")!.effects.filter((effect) => effect.kind === "damage")).toHaveLength(5); expect(abilityById("rom-mage-primary-12")!.effects.filter((effect) => effect.kind === "damage")).toHaveLength(3); expect(abilityById("rom-mage-primary-8")!.radius).toBeGreaterThan(0); expect(abilityById("rom-mage-primary-2")!.effects.some((effect) => ( effect.kind === "apply-aura" && (effect.aura.absorbs?.length ?? 0) > 0 ))).toBe(true); expect(abilityById("rom-mage-primary-56")!.effects).toEqual([ expect.objectContaining({ kind: "status", maxTargets: 1, breakOnDamage: true }), ]); expect(elite(30).effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "apply-aura", aura: expect.objectContaining({ modifiers: expect.arrayContaining([ expect.objectContaining({ kind: "stat", stat: "threat", operation: "percent", value: -0.1 }), ]), }), }), ])); expect(elite(30).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.utilityTags?.includes(ROM_BREATH_ERASE_THREAT_TAG))).toBe(true); }); it("uses magic schools for Mage and Warlock magic projectiles and dark spells", () => { for (const [id, school] of [ ["rom-mage-primary-1", "fire"], ["rom-mage-primary-52", "nature"], ["rom-warlock-general-0", "shadow"], ["rom-warlock-general-4", "shadow"], ["rom-warlock-general-5", "shadow"], ] as const) { expect(abilityDamageSchool("rom-mage", abilityById(id)!), id).toBe(school); } }); it("damages the selected target using both class resources", () => { initialize(30); registerTarget(); const manaBefore = useCombatStore.getState().resourcePools.mana!; expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 1_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 4_100); expect(useCombatStore.getState().mobs["mage-warlock-dummy"]!.health).toBeLessThan(100_000); expect(useCombatStore.getState().resourcePools.mana).toBeLessThan(manaBefore); initialize(30); registerTarget("warlock-dummy"); const focusBefore = useCombatStore.getState().resourcePools.focus!; expect(useCombatStore.getState().castAbility("rom-warlock-general-0", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 3_100); expect(useCombatStore.getState().mobs["warlock-dummy"]!.health).toBeLessThan(100_000); expect(useCombatStore.getState().resourcePools.focus).toBeLessThan(focusBefore); }); it("Electrostatic Charge creates its source percentage of maximum HP absorb", () => { initialize(60); const before = useCombatStore.getState(); expect(before.castAbility("rom-mage-primary-2", undefined, 1_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 1_100); const charge = useCombatStore.getState().auras.find((aura) => ( aura.definition.name.includes("Electrostatic Charge") && aura.absorbs.length > 0 )); expect(charge).toBeDefined(); expect(charge!.absorbs[0]?.remaining).toBe(Math.round(before.maxHealth * 0.3)); }); it("hits nearby enemies with Mage and Warlock area spells, respecting the source target caps", () => { initialize(30); for (const [id, x] of [["area-a", 2], ["area-b", 3], ["area-c", 4], ["area-far", 20]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 }); setMobPosition(id, [x, 0, 0]); } useCombatStore.getState().selectMob("area-a"); expect(useCombatStore.getState().castAbility("rom-mage-primary-8", undefined, 1_000)).toMatchObject({ ok: true }); for (const id of ["area-a", "area-b", "area-c"]) { expect(useCombatStore.getState().mobs[id]!.health, `Purgatory Fire ${id}`).toBeLessThan(100_000); } expect(useCombatStore.getState().mobs["area-far"]!.health).toBe(100_000); initialize(30); for (const [id, x] of [["weave-a", 2], ["weave-b", 3], ["weave-c", 4], ["weave-far", 20]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 }); setMobPosition(id, [x, 0, 0]); } useCombatStore.getState().selectMob("weave-a"); expect(useCombatStore.getState().castAbility("rom-warlock-general-1", undefined, 1_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 2_100); for (const id of ["weave-a", "weave-b", "weave-c"]) { expect(useCombatStore.getState().mobs[id]!.health, `Weakening Weave ${id}`).toBeLessThan(100_000); } expect(useCombatStore.getState().mobs["weave-far"]!.health).toBe(100_000); }); it("caps Surge of Malice at three targets even when four enemies are nearby", () => { initialize(30); for (const [id, x] of [ ["surge-a", 2], ["surge-b", 3], ["surge-c", 4], ["surge-d", 5], ["surge-far", 20], ] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 }); setMobPosition(id, [x, 0, 0]); } useCombatStore.getState().selectMob("surge-a"); expect(useCombatStore.getState().castAbility("rom-warlock-general-5", undefined, 1_000)) .toMatchObject({ ok: true }); const damaged = ["surge-a", "surge-b", "surge-c", "surge-d"] .filter((id) => useCombatStore.getState().mobs[id]!.health < 100_000); expect(damaged).toHaveLength(3); expect(useCombatStore.getState().mobs["surge-far"]!.health).toBe(100_000); }); it("Static Resonance resets only Electrostatic Charge", () => { initialize(60); useCombatStore.setState({ cooldowns: { "rom-mage-primary-2": 90_000, "rom-mage-primary-9": 90_000, "rom-mage-general-0": 90_000, } }); expect(useCombatStore.getState().castAbility(elite(15).id, undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(0); expect(useCombatStore.getState().cooldowns["rom-mage-primary-9"]).toBe(90_000); expect(useCombatStore.getState().cooldowns["rom-mage-general-0"]).toBe(90_000); }); it("Deep Inspiration resets Intensification and Energy Well only", () => { initialize(60); useCombatStore.setState({ cooldowns: { "rom-mage-general-3": 90_000, "rom-mage-primary-10": 90_000, "rom-mage-primary-2": 90_000, } }); expect(useCombatStore.getState().castAbility(elite(45).id, undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().cooldowns["rom-mage-general-3"]).toBe(0); expect(useCombatStore.getState().cooldowns["rom-mage-primary-10"]).toBe(0); expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(90_000); }); it("Soul Stepping permits a cast to finish after the Mage moves", () => { initialize(60); registerTarget("ordinary-moving-cast-dummy"); expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().setPlayerPosition([1, 0, 0]); useCombatStore.getState().tick(0.1, 4_100); expect(useCombatStore.getState().mobs["ordinary-moving-cast-dummy"]!.health).toBe(100_000); initialize(60); useCombatStore.getState().setPlayerPosition([0, 0, 0]); registerTarget("moving-cast-dummy"); expect(useCombatStore.getState().castAbility(elite(40).id, undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 2_600)) .toMatchObject({ ok: true }); useCombatStore.getState().setPlayerPosition([1, 0, 0]); useCombatStore.getState().tick(0.1, 5_700); expect(useCombatStore.getState().mobs["moving-cast-dummy"]!.health).toBeLessThan(100_000); }); it("Soul Speed adds its movement and spell speed buffs when Soul Stepping is cast", () => { initialize(60); expect(useCombatStore.getState().castAbility(elite(40).id, undefined, 1_000)) .toMatchObject({ ok: true }); const modifiers = useCombatStore.getState().auras .filter((aura) => aura.targetId === PLAYER_AGGRO_ID) .flatMap((aura) => aura.definition.modifiers ?? []); expect(modifiers).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "stat", stat: "movement-speed", value: 0.2 }), expect.objectContaining({ kind: "stat", stat: "spell-haste", value: 20 }), ])); }); it("Fire Lightning Burst hits twice and resets its own cooldown only if the target survives", () => { initialize(60); registerTarget(); useCombatStore.setState({ cooldowns: { "rom-mage-primary-2": 90_000 } }); expect(elite(50).effects.filter((effect) => effect.kind === "damage")).toHaveLength(2); expect(useCombatStore.getState().castAbility(elite(50).id, undefined, 1_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 3_100); expect(useCombatStore.getState().mobs["mage-warlock-dummy"]!.health).toBeLessThan(100_000); expect(useCombatStore.getState().cooldowns[elite(50).id] ?? 0).toBeLessThanOrEqual(3_100); expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(90_000); expect(useCombatStore.getState().castAbility(elite(50).id, undefined, 4_600)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 6_700); expect(useCombatStore.getState().cooldowns[elite(50).id]).toBeGreaterThan(6_700); }); it("does not apply conditional passive buffs or enemy defense penalties to the caster on login", () => { initialize(79); const auraNames = useCombatStore.getState().auras.map((aura) => aura.definition.name); for (const level of [25, 35, 60, 70]) { expect(auraNames.some((name) => name.startsWith(elite(level).name)), elite(level).name).toBe(false); } expect(elite(20).effects.some((effect) => effect.kind === "scripted")).toBe(false); }); it("uses Industry Fire's hidden Soul Pain payload during a live cast", () => { initialize(60); registerTarget("industry-fire-dummy"); expect(useCombatStore.getState().castAbility("rom-warlock-general-4", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 3_100); const state = useCombatStore.getState(); expect(state.mobs["industry-fire-dummy"]!.health).toBeLessThan(100_000); expect(state.effects.some((effect) => effect.tag === "rom-industry-fire" && effect.targetId === "industry-fire-dummy")).toBe(true); }); it("triggers Lightning Display and Elements of Pride only after their source spells land", () => { initialize(60); registerTarget("elite-trigger-dummy"); expect(useCombatStore.getState().castAbility("rom-mage-primary-52", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 3_100); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID && aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "magical-critical-strike" && modifier.value === 20))).toBe(true); expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 4_600)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 7_700); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID && aura.definition.id === "rom-mage-warlock-elements-nature" && aura.stacks === 1)).toBe(true); }); it("uses hidden skill 498746's exact fire hit and DoT after Industry Fire", () => { const soulPain = abilityById("rom-warlock-general-4")!; const ordinary = romMageWarlockCastEffects(soulPain, soulPain.effects, new Set()); const upgraded = romMageWarlockCastEffects(soulPain, soulPain.effects, new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire])); expect(ordinary.some((effect) => effect.kind === "damage")).toBe(false); expect(upgraded).toEqual([ expect.objectContaining({ kind: "damage", school: "fire", coefficient: 0, basePoints: 140, pointsPerLevel: 40, sourceLevel: 1, }), expect.objectContaining({ kind: "dot", school: "fire", coefficient: 0, basePoints: 23.4, pointsPerLevel: 10.4, sourceLevel: 1, ticks: 24, intervalMs: 1_000, }), ]); }); it("extends Charged, creates conditional Elements stacks, and enhances Soul Stepping", () => { const tags = new Set([ ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay, ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride, ROM_MAGE_WARLOCK_PASSIVE_TAGS.soulSpeed, ]); const plasma = abilityById("rom-mage-primary-52")!; const charged = romMageWarlockCastEffects(plasma, plasma.effects, tags).find((effect) => ( effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-charged") )); expect(charged).toMatchObject({ aura: { durationMs: 18_000 } }); const lightningBuff = romMageWarlockCastEffects(plasma, plasma.effects, tags).find((effect) => ( effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => ( modifier.kind === "stat" && modifier.stat === "magical-critical-strike" )) )); expect(lightningBuff).toMatchObject({ aura: { durationMs: 18_000, modifiers: expect.arrayContaining([ expect.objectContaining({ stat: "magical-critical-strike", value: 20 }), expect.objectContaining({ kind: "damage", school: "nature", value: 0.01 }), ]), }, }); const flame = abilityById("rom-mage-primary-1")!; expect(romMageWarlockCastEffects(flame, flame.effects, tags)).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "apply-aura", recipient: "caster", aura: expect.objectContaining({ id: "rom-mage-warlock-elements-nature", maxStacks: 2 }), }), ])); const stepping = elite(40); expect(romMageWarlockCastEffects(stepping, stepping.effects, new Set()).some((effect) => ( effect.kind === "apply-aura" && [24, 51].includes(effect.sourceAuraType) ))).toBe(false); expect(romMageWarlockCastEffects(stepping, stepping.effects, tags).some((effect) => ( effect.kind === "apply-aura" && [24, 51].includes(effect.sourceAuraType) ))).toBe(true); }); it("keeps Elemental Extraction's defense debuff on a critical-hit target", () => { const passive = new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]); expect(romMageWarlockCriticalEffects(abilityById("rom-mage-primary-1")!, passive)).toEqual([]); const electricExplosion = abilityById("rom-mage-primary-54")!; expect(romMageWarlockCriticalEffects(electricExplosion, new Set())).toEqual([]); expect(romMageWarlockCriticalEffects(electricExplosion, passive)).toEqual([ expect.objectContaining({ kind: "apply-aura", recipient: "ability-target", aura: expect.objectContaining({ disposition: "debuff", durationMs: 10_000, modifiers: expect.arrayContaining([ expect.objectContaining({ stat: "armor", value: -0.3 }), expect.objectContaining({ stat: "magical-defense", value: -0.3 }), ]), }), }), ]); }); it("triggers Elemental Extraction's target debuff on a live critical Electric Explosion hit", () => { initialize(79); registerTarget("critical-extraction-dummy"); useCombatStore.setState((state) => ({ auras: applyCombatAura(state.auras, { definition: { id: "mage-warlock-test-guaranteed-crit", name: "Guaranteed critical test trigger", disposition: "buff", durationMs: 10_000, utilityTags: ["next-offensive-critical"], }, sourceId: PLAYER_AGGRO_ID, targetId: PLAYER_AGGRO_ID, now: 1_000, }).auras, })); expect(useCombatStore.getState().castAbility("rom-mage-primary-54", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 4_100); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === "critical-extraction-dummy" && aura.definition.id === "rom-mage-warlock-elemental-extraction-debuff")).toBe(true); }); it("uses hidden 499851's stronger normal Electric Explosion branch at level 70", () => { const explosion = abilityById("rom-mage-primary-54")!; const original = explosion.effects.filter((effect) => effect.kind === "damage"); const upgraded = romMageWarlockCastEffects(explosion, explosion.effects, new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction])) .filter((effect) => effect.kind === "damage"); expect(upgraded).toHaveLength(2); expect(upgraded[0]).toEqual(original[0]); expect(upgraded[1]).toMatchObject({ school: "nature", coefficient: 0, basePoints: 65, pointsPerLevel: 15, sourceLevel: 1, }); }); it("honors Fire Lightning Burst's source six-second reset guard", () => { const burst = elite(50); expect(romFireLightningBurstReset(burst, true, false)).toEqual({ reset: true, guardDurationMs: ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS, }); expect(romFireLightningBurstReset(burst, true, true)).toEqual({ reset: false, guardDurationMs: 0 }); expect(romFireLightningBurstReset(burst, false, false)).toEqual({ reset: false, guardDurationMs: 0 }); expect(romFireLightningBurstReset(elite(45), true, false)).toEqual({ reset: false, guardDurationMs: 0 }); }); it("exposes Breath Erase's five-second threat reduction to the runtime", () => { const breath = elite(30).effects.find((effect) => effect.kind === "apply-aura" && effect.aura.utilityTags?.includes(ROM_BREATH_ERASE_THREAT_TAG)); expect(breath).toMatchObject({ aura: { durationMs: 5_000 } }); expect(romMageWarlockThreatScale(new Set())).toBe(1); expect(romMageWarlockThreatScale(new Set([ROM_BREATH_ERASE_THREAT_TAG]))).toBe(0.9); }); it("reduces live spell threat while Breath Erase is active", () => { const castFlame = (useBreath: boolean) => { initialize(60); registerTarget("breath-threat-dummy"); if (useBreath) { expect(useCombatStore.getState().castAbility(elite(30).id, undefined, 1_000)) .toMatchObject({ ok: true }); } expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 2_600)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 5_700); const target = useCombatStore.getState().mobs["breath-threat-dummy"]!; return { damage: target.maxHealth - target.health, threat: target.threatByActor[PLAYER_AGGRO_ID] ?? 0, }; }; const baseline = castFlame(false); const reduced = castFlame(true); expect(reduced.damage).toBeGreaterThan(0); expect(reduced.damage).toBe(baseline.damage); expect((reduced.threat - 1) / (baseline.threat - 1)).toBeCloseTo(0.9, 5); }); it("applies Elemental Extraction's defense reduction to Electric Explosion's live magic mitigation", () => { const targetId = "extraction-defense-dummy"; const castElectricExplosion = (useDebuff: boolean) => { initialize(60); useCombatStore.getState().registerMob(targetId, { name: targetId, maxHealth: 100_000, resistances: { nature: 1_400 }, }); setMobPosition(targetId, [3, 0, 0]); useCombatStore.getState().selectMob(targetId); if (useDebuff) { const rider = romMageWarlockCriticalEffects(abilityById("rom-mage-primary-54")!, new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))[0]; expect(rider.kind).toBe("apply-aura"); if (rider.kind !== "apply-aura") throw new Error("Elemental Extraction debuff missing"); useCombatStore.setState((state) => ({ auras: applyCombatAura(state.auras, { definition: rider.aura, sourceId: PLAYER_AGGRO_ID, targetId, now: 1_000, }).auras, })); } expect(useCombatStore.getState().castAbility("rom-mage-primary-54", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 4_100); return useCombatStore.getState().mobs[targetId]!.maxHealth - useCombatStore.getState().mobs[targetId]!.health; }; const baselineDamage = castElectricExplosion(false); const reducedDefenseDamage = castElectricExplosion(true); expect(baselineDamage).toBeGreaterThan(0); expect(reducedDefenseDamage).toBeGreaterThan(baselineDamage); }); });