import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import type { ActiveAura } from "./combatAuras"; const ELECTROSTATIC_CHARGE_EXPERTISE_SPELL_ID = 498_871; const ELECTROSTATIC_CHARGE_SPELL_ID = 491_156; export const ROM_MAGE_MEDITATIVE_CURRENT = Object.freeze({ passiveSpellId: 493_262, triggerSpellIds: [491_150, 491_169] as const, durationMs: 10_000, cooldownMs: 30_000, spellHaste: 10, cooldownAuraTag: "rom-mage-meditative-current-cooldown", }); export const ROM_MAGE_MEDITATIVE_CURRENT_COOLDOWN_EFFECT: Extract = { kind: "apply-aura", recipient: "caster", aura: { id: "rom-mage-meditative-current-cooldown", name: "Meditative Current cooldown", disposition: "buff", durationMs: ROM_MAGE_MEDITATIVE_CURRENT.cooldownMs, hideFromAuraStrip: true, utilityTags: [ROM_MAGE_MEDITATIVE_CURRENT.cooldownAuraTag], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: ROM_MAGE_MEDITATIVE_CURRENT.passiveSpellId, }; export const ROM_MAGE_KNIGHT_LIGHT_OF_DAY = Object.freeze({ passiveSpellId: 493_034, triggerSpellIds: [491_594, 492_634] as const, durationMs: 15_000, cooldownMs: 20_000, lightDamagePercent: 8, cooldownAuraTag: "rom-mage-knight-light-of-day-cooldown", }); export const ROM_MAGE_KNIGHT_LIGHT_OF_DAY_COOLDOWN_EFFECT: Extract = { kind: "apply-aura", recipient: "caster", aura: { id: "rom-mage-knight-light-of-day-cooldown", name: "Light of Day cooldown", disposition: "buff", durationMs: ROM_MAGE_KNIGHT_LIGHT_OF_DAY.cooldownMs, hideFromAuraStrip: true, utilityTags: [ROM_MAGE_KNIGHT_LIGHT_OF_DAY.cooldownAuraTag], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: ROM_MAGE_KNIGHT_LIGHT_OF_DAY.passiveSpellId, }; /** Find Light of Day's active internal cooldown marker for one caster. */ export function romMageKnightLightOfDayReadyAt( activeAuras: readonly Pick[], casterId: string, now: number, ): number { const cooldown = activeAuras.find((aura) => aura.targetId === casterId && aura.definition.utilityTags?.includes(ROM_MAGE_KNIGHT_LIGHT_OF_DAY.cooldownAuraTag) && (aura.expiresAt === null || aura.expiresAt > now)); return cooldown ? cooldown.expiresAt ?? Number.POSITIVE_INFINITY : now; } export interface RomMageKnightLightOfDayResult { readonly effect: AbilityEffect | null; readonly nextReadyAt: number; } /** * RuneWaker Light of Day (493034): a landed Holy Light Strike (491594) or * Light Charge (492634) hit grants +8% Holy damage for 15 seconds, with a * 20-second internal cooldown. The effect only belongs to Mage/Knight builds. */ export function romMageKnightLightOfDayProc( classId: string | null, secondaryClassId: string | null | undefined, ability: Pick, learnedPassiveSpellIds: readonly number[], landed: boolean, readyAt: number, now: number, ): RomMageKnightLightOfDayResult { const eligible = classId === "rom-mage" && secondaryClassId === "rom-knight" && ability.classId === "rom-mage" && ability.secondaryClassId === "rom-knight" && ability.source === "runewaker" && ROM_MAGE_KNIGHT_LIGHT_OF_DAY.triggerSpellIds.includes(ability.dbcSpellId as 491_594 | 492_634) && learnedPassiveSpellIds.includes(ROM_MAGE_KNIGHT_LIGHT_OF_DAY.passiveSpellId) && landed && now >= readyAt; if (!eligible) return { effect: null, nextReadyAt: readyAt }; return { effect: { kind: "apply-aura", recipient: "caster", aura: { id: "rom-mage-knight-light-of-day", name: "Light of Day", description: "A landed Holy Light Strike or Light Charge hit grants +8% Holy damage for 15 seconds. This effect has a 20-second internal cooldown.", disposition: "buff", durationMs: ROM_MAGE_KNIGHT_LIGHT_OF_DAY.durationMs, maxStacks: 1, stackBehavior: "refresh", utilityTags: ["rom-mage-knight-light-of-day-active"], modifiers: [{ kind: "damage", direction: "dealt", school: "holy", operation: "percent", value: ROM_MAGE_KNIGHT_LIGHT_OF_DAY.lightDamagePercent / 100, }], }, sourceEffectType: -1, sourceAuraType: 49, miscValue: ROM_MAGE_KNIGHT_LIGHT_OF_DAY.lightDamagePercent, triggerSpellId: ROM_MAGE_KNIGHT_LIGHT_OF_DAY.passiveSpellId, }, nextReadyAt: now + ROM_MAGE_KNIGHT_LIGHT_OF_DAY.cooldownMs, }; } /** Find the active per-caster internal cooldown marker, if one exists. */ export function romMageMeditativeCurrentReadyAt( activeAuras: readonly Pick[], casterId: string, now: number, ): number { const cooldown = activeAuras.find((aura) => aura.targetId === casterId && aura.definition.utilityTags?.includes(ROM_MAGE_MEDITATIVE_CURRENT.cooldownAuraTag) && (aura.expiresAt === null || aura.expiresAt > now)); return cooldown ? cooldown.expiresAt ?? Number.POSITIVE_INFINITY : now; } export interface RomMageMeditativeCurrentResult { readonly effect: AbilityEffect | null; readonly nextReadyAt: number; } /** * RuneWaker Meditative Current (493262): a landed Flame (491150) or Plasma * Arrow (491169) hit grants +10 spell haste for 10 seconds, with a 30-second * internal cooldown. Cooldown readiness is passed in by the combat event owner. */ export function romMageMeditativeCurrentProc( classId: string | null, ability: Pick, learnedPassiveSpellIds: readonly number[], landed: boolean, readyAt: number, now: number, ): RomMageMeditativeCurrentResult { const eligible = classId === "rom-mage" && ability.source === "runewaker" && ROM_MAGE_MEDITATIVE_CURRENT.triggerSpellIds.some((spellId) => spellId === ability.dbcSpellId) && learnedPassiveSpellIds.includes(ROM_MAGE_MEDITATIVE_CURRENT.passiveSpellId) && landed && now >= readyAt; if (!eligible) return { effect: null, nextReadyAt: readyAt }; return { effect: { kind: "apply-aura", recipient: "caster", aura: { id: "rom-mage-meditative-current", name: "Meditative Current", description: "A landed Flame or Plasma Arrow hit grants +10 spell haste for 10 seconds. This effect has a 30-second internal cooldown.", disposition: "buff", durationMs: ROM_MAGE_MEDITATIVE_CURRENT.durationMs, maxStacks: 1, stackBehavior: "refresh", utilityTags: ["rom-mage-meditative-current-active"], modifiers: [{ kind: "stat", stat: "spell-haste", operation: "flat", value: ROM_MAGE_MEDITATIVE_CURRENT.spellHaste, }], }, sourceEffectType: -1, sourceAuraType: 51, miscValue: -ROM_MAGE_MEDITATIVE_CURRENT.spellHaste, triggerSpellId: ROM_MAGE_MEDITATIVE_CURRENT.passiveSpellId, }, nextReadyAt: now + ROM_MAGE_MEDITATIVE_CURRENT.cooldownMs, }; } /** Mage/Champion's level-45 passive shortens Electrostatic Charge by 30 seconds. */ export function romMageCooldownReductionMs( classId: string | null, secondaryClassId: string | null | undefined, ability: Pick, level: number, abilities: readonly Pick[], ): number { if (classId !== "rom-mage" || secondaryClassId !== "rom-champion" || ability.dbcSpellId !== ELECTROSTATIC_CHARGE_SPELL_ID || level < 45) return 0; return abilities.some((candidate) => candidate.passive && candidate.dbcSpellId === ELECTROSTATIC_CHARGE_EXPERTISE_SPELL_ID && candidate.unlockLevel <= level) ? 30_000 : 0; }