import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import type { RomGeneratedAbilityLike } from "./romAbilitySemantics"; /** The absorb amounts in Electrostatic Charge's source row are HP percentages. */ export const ROM_ELECTROSTATIC_PERCENT_ABSORB_TAG = "rom-electrostatic-percent-max-health"; export const ROM_MAGE_WARLOCK_PASSIVE_TAGS = Object.freeze({ eruption: "rom-mage-eruption", controlFlame: "rom-mage-druid-control-flame", controlFlameCooldown: "rom-mage-druid-control-flame-cooldown", flameFollower: "rom-mage-warden-flame-follower", industryFire: "rom-mage-warlock-industry-fire", lightningDisplay: "rom-mage-warlock-lightning-display", elementsOfPride: "rom-mage-warlock-elements-of-pride", soulSpeed: "rom-mage-warlock-soul-speed", elementalExtraction: "rom-mage-warlock-elemental-extraction", } as const); export const ROM_SOUL_STEPPING_CAST_TAG = "rom-mage-warlock-soul-stepping-cast"; export const ROM_BREATH_ERASE_THREAT_TAG = "rom-mage-warlock-breath-erase-threat"; export const ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG = "rom-fire-lightning-burst-reset-guard"; export const ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS = 6_000; // Hidden replacement skill 498746: 621504 is the initial Fire hit, and // 621503 is its 24-second Fire DoT. These values come from MagicObject.db. const INDUSTRY_FIRE_HIT_BASE = 140; const INDUSTRY_FIRE_HIT_PER_LEVEL = 40; const INDUSTRY_FIRE_DOT_BASE = 23.4; const INDUSTRY_FIRE_DOT_PER_LEVEL = 10.4; const MAGE_ERUPTION_PROC_CHANCE = 0.1; function scaledDamage(effect: AbilityEffect, factor: number): AbilityEffect { if (effect.kind !== "damage") return effect; return { ...effect, coefficient: effect.coefficient * factor, ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * factor }), ...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints * factor }), ...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel * factor }), ...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient * factor }), ...(effect.bonusFlat === undefined ? {} : { bonusFlat: effect.bonusFlat * factor }), }; } function controlFlameTriggeredAuras(): readonly AbilityEffect[] { return [ { kind: "apply-aura", recipient: "caster", aura: { id: "rom-mage-druid-control-flame-buff", name: "Control Flame", disposition: "buff", durationMs: 12_000, modifiers: [{ kind: "stat", stat: "magical-attack", operation: "percent", value: 0.08 }], }, sourceEffectType: -1, sourceAuraType: 171, miscValue: 8, triggerSpellId: 494054, }, { kind: "apply-aura", recipient: "caster", aura: { id: "rom-mage-druid-control-flame-cooldown", name: "Control Flame cooldown", disposition: "buff", durationMs: 20_000, utilityTags: [ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlameCooldown], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 494078, }, ]; } function stateAura(id: string, tag: string, durationMs: number | null): AbilityEffect { return { kind: "apply-aura", recipient: "caster", aura: { id: `rom-mage-warlock-state-${id}`, name: "Mage/Warlock class state", disposition: "buff", durationMs, maxStacks: 1, stackBehavior: "refresh", utilityTags: [tag], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 0, }; } /** * Source-specific mappings for the accepted Mage/Warlock build. These auras * are state markers; the combat runtime applies the conditional skill effects * only when the named source spells execute or critically hit their target. */ export function overrideRomMageWarlockEffects( row: RomGeneratedAbilityLike, translated: readonly AbilityEffect[], ): readonly AbilityEffect[] | null { if (row.sourceSkillId === 490221) return [stateAura("eruption", ROM_MAGE_WARLOCK_PASSIVE_TAGS.eruption, null)]; if (row.sourceSkillId === 494054) return [stateAura("control-flame", ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlame, null)]; if (row.sourceSkillId === 494051) return [stateAura("flame-follower", ROM_MAGE_WARLOCK_PASSIVE_TAGS.flameFollower, null)]; if (row.sourceSkillId === 491156) { return translated.map((effect) => { if (effect.kind !== "apply-aura" || !effect.aura.absorbs?.length) return effect; return { ...effect, aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_ELECTROSTATIC_PERCENT_ABSORB_TAG], }, }; }); } if (row.sourceSkillId === 498750) { return translated.map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_BREATH_ERASE_THREAT_TAG], }, } : effect); } if (row.sourceSkillId === 498745) return [stateAura("industry-fire", ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire, null)]; if (row.sourceSkillId === 498748) return [stateAura("lightning-display", ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay, null)]; if (row.sourceSkillId === 498751) return [stateAura("elements-of-pride", ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride, null)]; if (row.sourceSkillId === 499813) return [stateAura("soul-speed", ROM_MAGE_WARLOCK_PASSIVE_TAGS.soulSpeed, null)]; if (row.sourceSkillId === 499814) return [stateAura("elemental-extraction", ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction, null)]; if (row.sourceSkillId === 498757) return [ ...translated, stateAura("soul-stepping-cast", ROM_SOUL_STEPPING_CAST_TAG, 6_000), ]; return null; } function elementsOfPrideAura(school: "fire" | "nature", value: number): AbilityEffect { return { kind: "apply-aura", recipient: "caster", aura: { id: `rom-mage-warlock-elements-${school}`, name: `Elements of Pride: ${school}`, disposition: "buff", durationMs: 13_000, maxStacks: 2, stackBehavior: "add", modifiers: [{ kind: "damage", direction: "dealt", school, operation: "percent", value, perStack: true }], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 498751, }; } /** Add pair-elite riders to an executable cast after conditional source selection. */ export function romMageWarlockCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], passiveTags: ReadonlySet, eruptionProcRoll = 1, ): readonly AbilityEffect[] { if (ability.source !== "runewaker") return effects; if ([490204, 491150].includes(ability.dbcSpellId) && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.eruption) && eruptionProcRoll < MAGE_ERUPTION_PROC_CHANCE) { const doubledDamage = effects.map((effect) => scaledDamage(effect, 2)); return passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlame) && !passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlameCooldown) ? [...doubledDamage, ...controlFlameTriggeredAuras()] : doubledDamage; } if (ability.dbcSpellId === 497964 && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire)) { return [ { kind: "damage", school: "fire", coefficient: 0, basePoints: INDUSTRY_FIRE_HIT_BASE, pointsPerLevel: INDUSTRY_FIRE_HIT_PER_LEVEL, sourceLevel: 1, }, { kind: "dot", school: "fire", coefficient: 0, basePoints: INDUSTRY_FIRE_DOT_BASE, pointsPerLevel: INDUSTRY_FIRE_DOT_PER_LEVEL, sourceLevel: 1, ticks: 24, intervalMs: 1_000, tag: "rom-industry-fire", }, ]; } let result = effects; if (passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay)) { result = result.map((effect) => { if (effect.kind === "apply-aura" && [491169, 498749].includes(ability.dbcSpellId)) { if (effect.aura.utilityTags?.includes("rom-charged")) { return { ...effect, aura: { ...effect.aura, durationMs: 18_000 } }; } if (effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "magical-critical-strike")) { return { ...effect, aura: { ...effect.aura, durationMs: 18_000, modifiers: [ ...effect.aura.modifiers, { kind: "damage" as const, direction: "dealt" as const, school: "nature" as const, operation: "percent" as const, value: 0.01 }, ], }, }; } } return effect; }); } if (passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride)) { if ([491150, 498755, 499812].includes(ability.dbcSpellId)) { result = [...result, elementsOfPrideAura("nature", 0.02)]; } if ([491170, 498756, 499851].includes(ability.dbcSpellId)) { result = [...result, elementsOfPrideAura("fire", 0.01)]; } } if (ability.dbcSpellId === 491170 && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction)) { // Hidden level-70 Electric Explosion 499851 replaces the normal branch's // 620232 hit with 623668; its Charged 623669 hit keeps the same values. const damage = result.filter((effect) => effect.kind === "damage"); const normal = damage.at(-1); if (damage.length >= 2 && normal) result = result.map((effect) => effect === normal ? { kind: "damage" as const, school: "nature" as const, coefficient: 0, basePoints: 65, pointsPerLevel: 15, sourceLevel: 1, } : effect); } if (ability.dbcSpellId === 498757 && !passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.soulSpeed)) { result = result.filter((effect) => effect.kind !== "apply-aura" || (effect.sourceAuraType !== 24 && effect.sourceAuraType !== 51)); } return result; } /** Apply only after Electric Explosion or source 499812 critically hits. */ export function romMageWarlockCriticalEffects( ability: AbilityDefinition, passiveTags: ReadonlySet, ): readonly AbilityEffect[] { if (!passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction) || ![491150, 491170, 498755, 498756, 499812, 499851].includes(ability.dbcSpellId)) return []; return [{ kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-mage-warlock-elemental-extraction-debuff", name: "Elemental Extraction", disposition: "debuff", durationMs: 10_000, maxStacks: 1, stackBehavior: "refresh", modifiers: [ { kind: "stat", stat: "armor", operation: "percent", value: -0.3 }, { kind: "stat", stat: "magical-defense", operation: "percent", value: -0.3 }, ], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 499814, }]; } /** Lua script 02557: reset Burst on a surviving target once per six seconds. */ export function romFireLightningBurstReset( ability: AbilityDefinition, targetAlive: boolean, guardActive: boolean, ): { readonly reset: boolean; readonly guardDurationMs: number } { const reset = ability.dbcSpellId === 498759 && targetAlive && !guardActive; return { reset, guardDurationMs: reset ? ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS : 0 }; } /** Breath Erase source 621506: ten percent less generated threat for five seconds. */ export function romMageWarlockThreatScale(activeTags: ReadonlySet): number { return activeTags.has(ROM_BREATH_ERASE_THREAT_TAG) ? 0.9 : 1; }