import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import type { AuraProcDefinition } from "./combatAuras"; import type { RomGeneratedAbilityLike } from "./romAbilitySemantics"; export const ROM_KNIGHT_DIVINE_PUNISHMENT_TAG = "rom-divine-punishment"; export const ROM_KNIGHT_GODS_VENGEANCE_TAG = "rom-gods-vengeance"; export const ROM_KNIGHT_SHIELD_OF_VALOR_TAG = "rom-shield-of-valor"; export const ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG = "rom-compassion-of-holy-light"; export const ROM_KNIGHT_HOLY_ILLUMINATION_TAG = "rom-holy-illumination"; export const ROM_KNIGHT_MASTER_OF_PARRY_TAG = "rom-knight-master-of-parry"; export const ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC = "rom-knight-master-of-parry-block"; export const ROM_KNIGHT_PANTHERS_BLESSING_TAG = "rom-knight-panthers-blessing"; export const ROM_KNIGHT_DEADLY_WHIRLWIND_TAG = "rom-deadly-whirlwind"; export const ROM_KNIGHT_CURRENT_COA_WHIRLWIND_SHIELD_ID = 1_493_719; export const ROM_KNIGHT_LOCAL_WHIRLWIND_SHIELD_ID = 490_175; export interface RomKnightGodsVengeanceResetContext { readonly characterClassId: string; readonly secondaryClassId: string | null | undefined; readonly ability: Pick; readonly learnedPassiveSpellIds: readonly number[]; readonly shadowStepLearned: boolean; readonly targetSealStacks: number; readonly sameTarget: boolean; readonly landed: boolean; } /** Current CoA God's Vengeance resets Shadow Step only for its linked Punishment hit at exactly four Seals. */ export function romKnightGodsVengeanceShouldResetShadowStep( context: RomKnightGodsVengeanceResetContext, ): boolean { return context.characterClassId === "rom-knight" && context.secondaryClassId === "rom-rogue" && context.ability.source === "runewaker" && context.ability.classId === "rom-knight" && context.ability.secondaryClassId === "rom-rogue" && context.ability.dbcSpellId === 499_909 && context.learnedPassiveSpellIds.includes(499_934) && context.shadowStepLearned && context.targetSealStacks === 4 && context.sameTarget && context.landed; } /** Shield of Valor reduces Holy Seal's remaining cooldown only on an active-aura parry. */ export function romKnightShieldOfValorCooldownReductionMs(auraActive: boolean, parried: boolean): number { return auraActive && parried ? 1_000 : 0; } export interface RomKnightWarriorWhirlwindAdjustment { readonly rageCostReduction: number; readonly cooldownReductionMs: number; } /** RuneWaker Knight/Warrior passive 491639 modifies only Warrior Whirlwind. */ export function romKnightWarriorWhirlwindAdjustment( characterClassId: string, secondaryClassId: string | null | undefined, ability: Pick, learnedPassiveSpellIds: readonly number[], ): RomKnightWarriorWhirlwindAdjustment { if (characterClassId !== "rom-knight" || secondaryClassId !== "rom-warrior" || ability.source !== "runewaker" || ability.classId !== "rom-warrior" || ability.dbcSpellId !== 490133 || !learnedPassiveSpellIds.includes(491639)) { return { rageCostReduction: 0, cooldownReductionMs: 0 }; } // The local 491639 tooltip explicitly says 10 Rage and 20 seconds. return { rageCostReduction: 10, cooldownReductionMs: 20_000 }; } /** Current CoA changed God's Sword to passive, but the current passive payload is unpublished. */ export function romKnightRogueHolyStrikeCriticalRateIncreasePercent( characterClassId: string, secondaryClassId: string | null | undefined, ability: Pick, learnedPassiveSpellIds: readonly number[], ): number { // The RuneWaker snapshot's 40% Holy Strike bonus conflicts with current CoA, // which only says God's Sword became passive and does not publish its payload. void characterClassId; void secondaryClassId; void ability; void learnedPassiveSpellIds; return 0; } function hasPassiveTag( passiveTags: readonly string[] | ReadonlySet, tag: string, ): boolean { return Array.isArray(passiveTags) ? passiveTags.includes(tag) : (passiveTags as ReadonlySet).has(tag); } /** CoA 6-1: Deadly Wound reduces healing received by 30%; player targets last 5s. */ export function romKnightDeadlyWhirlwindAura(targetIsPlayer: boolean): Extract { return { kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-knight-deadly-wound", name: "Deadly Wound", disposition: "debuff", durationMs: targetIsPlayer ? 5_000 : 10_000, dispelCategory: "magic", modifiers: [{ kind: "healing", direction: "taken", operation: "percent", value: -0.3 }], utilityTags: [ROM_KNIGHT_DEADLY_WHIRLWIND_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 491_639, }; } /** CoA 10.6.1: Divine Punishment makes the target take 2% more damage per Seal for 10s. */ export function romKnightDivinePunishmentAuraForSealStacks( sealStacks: number, ): Extract | null { const stacks = Math.max(0, Math.min(4, Math.trunc(sealStacks))); if (stacks <= 0) return null; return { kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-knight-divine-punishment-damage-taken", name: "Divine Punishment", disposition: "debuff", durationMs: 10_000, dispelCategory: "magic", modifiers: [{ kind: "damage", direction: "taken", operation: "percent", value: stacks * 0.02 }], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: stacks * 2, triggerSpellId: 491_608, }; } /** Current CoA 11.1.5 cooldown rider, bridged by the exact Whirlwind Shield name. */ export function romKnightCompassionWhirlwindShieldCooldownReductionMs( characterClassId: string, secondaryClassId: string | null | undefined, ability: Pick, learnedPassiveSpellIds: readonly number[], ): number { return characterClassId === "rom-knight" && secondaryClassId === "rom-priest" && ability.source === "runewaker" && ability.classId === "rom-knight" && (ability.dbcSpellId === ROM_KNIGHT_LOCAL_WHIRLWIND_SHIELD_ID || ability.dbcSpellId === ROM_KNIGHT_CURRENT_COA_WHIRLWIND_SHIELD_ID) && learnedPassiveSpellIds.includes(491_635) ? 1_000 : 0; } /** CoA 11.1.5 Compassion heal formula; called only after a Whirlwind Shield hit lands. */ export function romKnightCompassionWhirlwindShieldHealAmount(stamina: number): number { return Math.max(0, Math.round(120 + Math.max(0, stamina) * 0.2)); } const ROM_KNIGHT_MASTER_OF_PARRY_PROC: AuraProcDefinition = { id: ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, triggers: "block", chance: 1, charges: 2, action: { kind: "custom", id: ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC }, }; function divinePunishmentPassive(row: RomGeneratedAbilityLike): readonly AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-knight-passive-491608", name: row.name, disposition: "buff", durationMs: null, utilityTags: [ROM_KNIGHT_DIVINE_PUNISHMENT_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 491608, }]; } function deadWhirlwindPassive(row: RomGeneratedAbilityLike): readonly AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-knight-passive-491639", name: row.name, disposition: "buff", durationMs: null, utilityTags: [ROM_KNIGHT_DEADLY_WHIRLWIND_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 491_639, }]; } function compassionOfHolyLightPassive(row: RomGeneratedAbilityLike): readonly AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-knight-passive-491635", name: "Compassion of Holy Light", disposition: "buff", durationMs: null, utilityTags: [ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 491_635, }]; } function holyIlluminationPassive(row: RomGeneratedAbilityLike): readonly AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-knight-passive-491632", name: row.name, disposition: "buff", durationMs: null, utilityTags: [ROM_KNIGHT_HOLY_ILLUMINATION_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 491_632, }]; } function panthersBlessingPassiveMarker(row: RomGeneratedAbilityLike): readonly AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-knight-passive-493047", name: row.name, disposition: "buff", durationMs: null, utilityTags: [ROM_KNIGHT_PANTHERS_BLESSING_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 493_047, }]; } /** Source-backed Knight passive and cast adjustments. */ export function overrideRomKnightEffects( row: RomGeneratedAbilityLike, translated: readonly AbilityEffect[], ): readonly AbilityEffect[] | null { if (row.classId === "rom-knight" && row.secondaryClassId === "rom-rogue" && row.sourceSkillId === 493047) { // Keep the stale RuneWaker shield-conditioned +2 Dodge child suppressed: // its chance and required-buff gate are absent. The current CoA dodge // restore is handled by romKnightPanthersBlessingRuntime.ts. return panthersBlessingPassiveMarker(row); } if (row.classId === "rom-knight" && row.secondaryClassId === "rom-warrior" && row.sourceSkillId === 490088) { // Fearless's local description promises Fear immunity, while its source // component only exposes the +3% Strength and Stamina modifiers. The // [SC_SKILLTIPS_FEAR] suffix is a tooltip category marker; discard the // generic parser's false-positive Fear control and attach immunity to its // actual stat buff rows. return translated .filter((effect) => !(effect.kind === "apply-aura" && effect.control?.kind === "fear")) .map((effect) => effect.kind === "apply-aura" && (effect.aura.modifiers?.length ?? 0) > 0 ? { ...effect, aura: { ...effect.aura, controlImmunities: [...new Set([...(effect.aura.controlImmunities ?? []), "fear" as const])], }, } : effect); } if (row.sourceSkillId === 492935) { const durationMs = Math.max(0, ...(row.source?.components ?? []).map((component) => ( Math.max(0, Number(component.effectTime ?? 0)) * 1_000 ))); return translated.map((effect) => { if (effect.kind !== "apply-aura") return effect; if (effect.aura.modifiers?.some((modifier) => ( modifier.kind === "stat" && modifier.stat === "parry" ))) return { ...effect, aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_KNIGHT_MASTER_OF_PARRY_TAG], procs: [...(effect.aura.procs ?? []), ROM_KNIGHT_MASTER_OF_PARRY_PROC], }, }; // Source component 503192 drives a periodic trigger for the same 15s // effect. Keep that hidden trigger aura on the component's lifetime too. if (effect.aura.procs?.some((proc) => proc.id.startsWith("rom-proc-trigger-492935-503192"))) return { ...effect, aura: { ...effect.aura, durationMs: durationMs || effect.aura.durationMs }, }; return effect; }); } if (row.sourceSkillId === 491639 && row.passive) return deadWhirlwindPassive(row); if (row.sourceSkillId === 491608 && row.passive) return divinePunishmentPassive(row); if (row.sourceSkillId === 499934 && row.passive) return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-knight-passive-499934", name: row.name, disposition: "buff", durationMs: null, utilityTags: [ROM_KNIGHT_GODS_VENGEANCE_TAG], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 499934, }]; if (row.sourceSkillId === 491635 && row.passive) return compassionOfHolyLightPassive(row); if (row.sourceSkillId === 491632 && row.passive) return holyIlluminationPassive(row); if (row.sourceSkillId === 490468) return translated.map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_KNIGHT_SHIELD_OF_VALOR_TAG], }, } : effect); return null; } /** Apply Divine Punishment's extracted +1% only to its named source skill. */ export function romKnightCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], passiveTags: readonly string[] | ReadonlySet = [], sealStacks = 0, ): readonly AbilityEffect[] { if (ability.source !== "runewaker" || (ability.classId !== "rom-knight" && ability.dbcSpellId !== 490_133)) return effects; let adjusted = effects; if (ability.dbcSpellId === 490_151 && hasPassiveTag(passiveTags, ROM_KNIGHT_DIVINE_PUNISHMENT_TAG)) { adjusted = adjusted.map((effect) => { if (effect.kind !== "damage" && effect.kind !== "dot") return effect; return { ...effect, coefficient: effect.coefficient * 1.285, ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * 1.285 }), ...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints * 1.285 }), ...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel * 1.285 }), ...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient * 1.285 }), }; }); const punishmentAura = romKnightDivinePunishmentAuraForSealStacks(sealStacks); if (punishmentAura) adjusted = [...adjusted, { ...punishmentAura, requiresLandedHit: true }]; } if (ability.dbcSpellId === 490_133 && hasPassiveTag(passiveTags, ROM_KNIGHT_DEADLY_WHIRLWIND_TAG)) { adjusted = [...adjusted, { ...romKnightDeadlyWhirlwindAura(false), requiresLandedHit: true }]; } if (ability.dbcSpellId === ROM_KNIGHT_LOCAL_WHIRLWIND_SHIELD_ID) { adjusted = adjusted.map((effect) => effect.kind === "damage" ? { ...effect, coefficient: 0, basePoints: 0, dieSides: 1, weaponDpsCoefficient: 7.014, school: "holy" as const, } : effect); if (hasPassiveTag(passiveTags, ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG)) { adjusted = [...adjusted, { kind: "heal", coefficient: 0, basePoints: 120, dieSides: 1, staminaCoefficient: 0.2, scope: "party", center: "caster", radius: 100, requiresLandedHit: true, }]; } if (hasPassiveTag(passiveTags, ROM_KNIGHT_HOLY_ILLUMINATION_TAG) || hasPassiveTag(passiveTags, ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG)) { adjusted = [...adjusted, { kind: "apply-aura", scope: "party", radius: 100, requiresLandedHit: true, aura: { id: "rom-knight-holy-illumination-proc", name: "Holy Illumination", disposition: "buff", durationMs: 10_000, modifiers: [{ kind: "damage", direction: "taken", operation: "percent", value: -0.15 }], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 491_632, }]; } } return adjusted; }