import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import type { AuraDefinition } from "./combatAuras"; import type { RomGeneratedAbilityLike } from "./romAbilitySemantics"; export const ROM_CHAMPION_FORGE_STACK_AURA: AuraDefinition = { id: "rom-champion-forge-stack", name: "Forge", description: "Each normal attack hit grants 2% physical attack for 8 seconds, stacking up to 5 times.", disposition: "buff", durationMs: 8_000, maxStacks: 5, stackBehavior: "add", modifiers: [{ kind: "stat", stat: "attack-power", operation: "percent", value: 0.02, perStack: true, }], }; export const ROM_CHAMPION_RUNE_OVERLOAD_TAG = "rom-champion-rune-overload"; export const ROM_CHAMPION_RUNE_OVERLOAD_RAGE_PER_SECOND = 15; export const ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID = "rom-champion-soul-forge-critical"; export const ROM_CHAMPION_SOUL_FORGE_PROFILE = Object.freeze({ sourceSpellId: 498652, criticalAuraComponentId: 621339, criticalAuraDurationMs: 300_000, darkPowerComponentId: 621341, trigger: "crit" as const, chance: 1, darkMagicAuraDurationMs: 10_000, darkMagicBonusPercent: 0.1, darkMagicSchool: "shadow" as const, }); export const ROM_CHAMPION_WAY_OF_FRENZY_PROC_ACTION_ID = "rom-champion-way-of-frenzy-hit"; export const ROM_CHAMPION_WAY_OF_FRENZY_PROFILE = Object.freeze({ sourceSpellId: 498720, sourceComponentId: 621458, triggerSpellId: 498715, triggerAbilityId: "rom-champion-rom-priest-elite-20", durationMs: 60_000, maxStacks: 5, staminaPercentPerStack: 0.02, }); const ROM_CHAMPION_FORGE_ACTIVE_AURA: AuraDefinition = { id: "rom-champion-forge-active", name: "Forge", description: "For 5 minutes, normal attack hits build Forge stacks and restore Rage.", disposition: "buff", durationMs: 300_000, utilityTags: ["rom-champion-forge-active"], procs: [{ id: "rom-champion-forge-normal-hit", triggers: ["hit", "crit"], attackKinds: ["melee"], chance: 1, action: { kind: "custom", id: "rom-champion-forge-hit" }, }], }; /** Replace the source component's unconditional stat aura with its described hit trigger. */ export function overrideRomChampionEffects( row: RomGeneratedAbilityLike, _translated: readonly AbilityEffect[], ): readonly AbilityEffect[] | null { if (row.classId !== "rom-champion" || row.secondaryClassId !== "rom-priest" || row.sourceSkillId !== ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId) return null; const component = row.source?.components?.find((entry) => ( entry.id === ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceComponentId )); const staminaModifier = component?.abilityModifiers?.find((modifier) => modifier.type === 162); const durationMs = Math.max(0, Math.round((component?.effectTime ?? 0) * 1_000)); if (!component || !staminaModifier || staminaModifier.value <= 0 || durationMs <= 0) return []; return [{ kind: "apply-aura", aura: { id: "rom-champion-way-of-frenzy-passive", name: row.name, disposition: "buff", durationMs: null, hideFromAuraStrip: true, procs: [{ id: "rom-champion-way-of-frenzy-divine-vengeance", triggers: ["hit", "crit"], abilityIds: [ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerAbilityId], chance: 1, action: { kind: "custom", id: ROM_CHAMPION_WAY_OF_FRENZY_PROC_ACTION_ID, data: { sourceSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId, sourceComponentId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceComponentId, triggerSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerSpellId, durationMs, maxStacks: 5, staminaPercentPerStack: staminaModifier.value / 100, }, }, }], }, recipient: "caster", sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId, }]; } function scaleDamage(effect: Extract, factor: number): AbilityEffect { 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 }), }; } function casterHealFromDamage(effect: Extract): AbilityEffect { return { kind: "heal", coefficient: effect.coefficient, ...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints }), ...(effect.dieSides === undefined ? {} : { dieSides: effect.dieSides }), ...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel }), ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel }), ...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient }), ...(effect.attackPowerCoefficient === undefined ? {} : { attackPowerCoefficient: effect.attackPowerCoefficient }), ...(effect.spiritCoefficient === undefined ? {} : { spiritCoefficient: effect.spiritCoefficient }), ...(effect.sourceLevel === undefined ? {} : { sourceLevel: effect.sourceLevel }), ...(effect.maxScalingLevel === undefined ? {} : { maxScalingLevel: effect.maxScalingLevel }), recipient: "caster", }; } function soulForgeMysteryEffects(effects: readonly AbilityEffect[]): readonly AbilityEffect[] { const critAuraIndex = effects.findIndex((effect) => effect.kind === "apply-aura" && effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.criticalAuraComponentId}-`)); const darkAuraIndex = effects.findIndex((effect) => effect.kind === "apply-aura" && effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.darkPowerComponentId}-`)); const critAuraEffect = effects[critAuraIndex]; const darkAuraEffect = effects[darkAuraIndex]; if (critAuraIndex < 0 || darkAuraIndex < 0 || critAuraEffect?.kind !== "apply-aura" || darkAuraEffect?.kind !== "apply-aura") return effects; const darkModifier = darkAuraEffect.aura.modifiers?.find((modifier) => modifier.kind === "damage" && modifier.direction === "dealt" && modifier.school === ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicSchool && modifier.operation === "percent"); if (darkModifier?.kind !== "damage") return effects; const output: AbilityEffect[] = []; effects.forEach((effect, index) => { if (index === darkAuraIndex) return; if (index === critAuraIndex && effect.kind === "apply-aura") { output.push({ ...effect, aura: { ...effect.aura, procs: [ ...(effect.aura.procs ?? []), { id: "rom-champion-soul-forge-dark-magic-on-crit", triggers: ROM_CHAMPION_SOUL_FORGE_PROFILE.trigger, chance: ROM_CHAMPION_SOUL_FORGE_PROFILE.chance, action: { kind: "custom", id: ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID, data: { sourceSpellId: ROM_CHAMPION_SOUL_FORGE_PROFILE.sourceSpellId, sourceComponentId: ROM_CHAMPION_SOUL_FORGE_PROFILE.darkPowerComponentId, auraId: "rom-champion-soul-forge-dark-magic", auraName: darkAuraEffect.aura.name, durationMs: darkAuraEffect.aura.durationMs ?? ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicAuraDurationMs, disposition: "buff", maxStacks: 1, stackBehavior: "refresh", target: "aura-source", modifierKind: "damage", modifierDirection: "dealt", modifierSchool: ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicSchool, modifierOperation: darkModifier.operation, modifierValue: darkModifier.value, }, }, }, ], }, }); return; } output.push(effect); }); return output; } /** Apply source-backed Champion cast branches that need runtime state. */ export function romChampionCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], targetHealthPercent: number, casterMaxHealth: number, ): readonly AbilityEffect[] { if (ability.source !== "runewaker" || ability.classId !== "rom-champion") return effects; switch (ability.dbcSpellId) { case 498652: // Soul Forge Mystery adds its 10-second dark magic buff on a critical attack. if (ability.secondaryClassId !== "rom-warlock") return effects; return soulForgeMysteryEffects(effects); case 498595: { // Rune Siphon's positive component 621264 restores the caster's HP. if (ability.secondaryClassId !== "rom-warlock") return effects; let healIndex = -1; for (let index = effects.length - 1; index >= 0; index -= 1) { if (effects[index]?.kind === "damage") { healIndex = index; break; } } if (healIndex < 0) return effects; return effects.flatMap((effect, index): AbilityEffect[] => { if (index === healIndex && effect.kind === "damage") return [casterHealFromDamage(effect)]; if (effect.kind === "heal" && effect.coefficient === 0 && effect.basePoints === undefined && effect.pointsPerLevel === undefined && effect.percentMaxHealth === undefined) return []; return [effect]; }); } case 498529: { // Rune Pulse restores Rage on a landed hit (component 622327). const restoreAura = effects.find((effect) => effect.kind === "apply-aura" && effect.aura.id.includes("-622327-") && effect.aura.modifiers?.some((modifier) => modifier.kind === "resource" && modifier.resource === "rage" && modifier.property === "regeneration")); if (restoreAura?.kind !== "apply-aura") return effects; const rageModifier = restoreAura.aura.modifiers?.find((modifier) => modifier.kind === "resource" && modifier.resource === "rage" && modifier.property === "regeneration"); const ragePerHit = rageModifier ? Math.max(0, Math.round(rageModifier.value * (restoreAura.aura.durationMs ?? 0) / 1_000)) : 0; const damageIndex = effects.findIndex((effect) => effect.kind === "damage"); if (ragePerHit <= 0 || damageIndex < 0) return effects; const output: AbilityEffect[] = []; effects.forEach((effect, index) => { if (index === damageIndex && effect.kind === "damage") { output.push({ ...effect, onDamageResource: { resource: "rage", amount: ragePerHit } }); } else if (effect !== restoreAura) { output.push(effect); } }); return output; } case 498584: // Champion/Warrior Determination Rune is a 30%-of-max-HP caster shield. if (ability.secondaryClassId !== "rom-warrior") return effects; return effects.map((effect) => effect.kind === "apply-aura" && effect.aura.absorbs?.length ? { ...effect, aura: { ...effect.aura, absorbs: effect.aura.absorbs.map((absorb) => ({ ...absorb, amount: Math.max(1, Math.round(Math.max(0, casterMaxHealth) * absorb.amount / 100)), })), }, } : effect); case 498523: // Rank 1 Forge's guaranteed on-normal-attack trigger is child skill 498524. return [{ kind: "apply-aura", aura: ROM_CHAMPION_FORGE_ACTIVE_AURA, recipient: "caster", sourceEffectType: 0, sourceAuraType: 0, miscValue: 0, triggerSpellId: 498524, }]; case 498572: // Rune Overload ends when its Rage drain reaches zero. return effects.map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_CHAMPION_RUNE_OVERLOAD_TAG], }, } : effect); case 498531: // Fearless Blow's +30% hit applies only below 30% target health. return effects.flatMap((effect) => { if (effect.kind === "apply-aura" && effect.aura.id.includes("-621179-")) return []; if (effect.kind === "apply-aura" && effect.aura.id.includes("-621183-")) { return [{ ...effect, recipient: "caster" as const }]; } if (effect.kind === "damage" && targetHealthPercent < 0.3) return [scaleDamage(effect, 1.3)]; return [effect]; }); case 498535: // Chain Drive's proc chance and damage bonus are absent from the source payload. return effects.filter((effect) => effect.kind !== "cooldown-reset"); case 498599: // Dark Energy Strike's cooldown proc chance is absent from the source payload. if (ability.secondaryClassId !== "rom-warlock") return effects; return effects.filter((effect) => effect.kind !== "cooldown-reset"); case 498651: // Heart Collection Rune explicitly resets Rune Siphon. if (ability.secondaryClassId !== "rom-warlock") return effects; return effects.map((effect) => effect.kind === "cooldown-reset" ? { ...effect, abilityName: "Rune Siphon" } : effect); default: return effects; } }