import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import type { RomGeneratedAbilityLike } from "./romAbilitySemantics"; import { romPriestChampionCastEffects } from "./romPriestChampionOverrides"; import { ROM_KNIGHT_SCOUT_BLOOD_PUNISHMENT_TAG, romKnightScoutBloodPunishmentActivationEffects, } from "./romKnightScoutBloodPunishment"; export const ROM_PRIEST_WARRIOR_BATTLE_MONK_STANCE_TAG = "rom-priest-warrior-battle-monk-stance"; const BATTLE_MONK_STANCE_GATED_SKILLS = new Set([491362, 491363, 491364, 492635, 493038]); export const ROM_PRIEST_KNIGHT_HOLY_POWER_HEAL_USES_TAG = "rom-priest-knight-holy-power-heal-uses"; export function romPriestWarriorStanceRequired( ability: Pick, ): boolean { return ability.source === "runewaker" && ability.classId === "rom-priest" && ability.secondaryClassId === "rom-warrior" && BATTLE_MONK_STANCE_GATED_SKILLS.has(ability.dbcSpellId); } /** Player and companion cast gates share the source-specific Priest/Warrior condition. */ export function romPriestWarriorStanceUseAllowed( ability: Pick, activeAuraTags: readonly string[] | ReadonlySet, ): boolean { if (!romPriestWarriorStanceRequired(ability)) return true; return Array.isArray(activeAuraTags) ? activeAuraTags.includes(ROM_PRIEST_WARRIOR_BATTLE_MONK_STANCE_TAG) : (activeAuraTags as ReadonlySet).has(ROM_PRIEST_WARRIOR_BATTLE_MONK_STANCE_TAG); } function passiveState(sourceId: number, name: string, tag: string): readonly AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: `rom-pair-passive-${sourceId}`, name, disposition: "buff", durationMs: null, utilityTags: [tag], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: sourceId, }]; } /** Source-specific pair mechanics that the generic component translator cannot infer. */ export function overrideRomPriestKnightEffects( row: RomGeneratedAbilityLike, translated: readonly AbilityEffect[], ): readonly AbilityEffect[] | null { // These RuneWaker party buffs are self-targeted in their source rows, and // their tooltip wording does not match the generic party-scope detector. // Preserve the locally translated components and apply their auras to the // party named by the source descriptions. const partyBuff = (row.classId === "rom-priest" && row.secondaryClassId === undefined && row.sourceSkillId === 490298) || (row.classId === "rom-priest" && row.secondaryClassId === "rom-scout" && row.sourceSkillId === 491323) || (row.classId === "rom-priest" && row.secondaryClassId === "rom-rogue" && row.sourceSkillId === 492932); if (partyBuff) { return translated.map((effect) => effect.kind === "apply-aura" && (effect.aura.modifiers?.length ?? 0) > 0 ? { ...effect, scope: "party" as const } : effect); } if (row.classId === "rom-priest" && row.secondaryClassId === "rom-warrior" && row.sourceSkillId === 491600) { return translated.map((effect) => effect.kind === "apply-aura" && effect.recipient === "caster" ? { ...effect, aura: { ...effect.aura, // All native Battle Monk stance components have a 900-second duration. durationMs: 900_000, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_PRIEST_WARRIOR_BATTLE_MONK_STANCE_TAG], }, } : effect); } switch (row.sourceSkillId) { case 492355: // Enhanced Grace of Life: Grace of Life also raises maximum MP. return passiveState(492355, row.name, "rom-enhanced-grace-of-life"); case 492361: // Life Link: Urgent Heal also heals its caster. return passiveState(492361, row.name, "rom-life-link"); case 499968: // Splendor Ripple: a triggered extra heal on source skill 499967. return passiveState(499968, row.name, "rom-splendor-ripple"); case 499957: // Light Guidance: activate healing bonus only with source state 499966. return passiveState(499957, row.name, "rom-light-guidance"); case 492638: // Lightning Rod: Lightning shocks targets already afflicted by Bone Chill. return passiveState(492638, row.name, "rom-priest-mage-lightning-rod"); case 492928: // Grand Dispel: Purify also removes harmful effects, beyond curses. return passiveState(492928, row.name, "rom-priest-mage-grand-dispel"); case 491610: // Enhanced Holy Strike: improves Holy Strike weapon damage. return passiveState(491610, row.name, "rom-enhanced-holy-strike"); case 490066: // Weaken Weapon Strength: Heavenly Arrow weakens target physical damage. return passiveState(490066, row.name, "rom-weaken-weapon-strength"); case 499926: // Evil Shackles: Holy Chains can root any creature with shorter duration. return passiveState(499926, row.name, "rom-evil-shackles"); case 499923: // Blood Punishment modifies Blood Arrow while its source aura is active. return passiveState(499923, row.name, ROM_KNIGHT_SCOUT_BLOOD_PUNISHMENT_TAG); case 491648: // Last Prayer affects only the next spell's cast time. return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-pair-last-prayer-next-spell", name: row.name, disposition: "buff", durationMs: null, utilityTags: ["rom-last-prayer"], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 51, miscValue: -1_000, triggerSpellId: 0, }]; case 493271: // Light Fairy has source lifetime -1; keep it until dismissal or session end. return translated.map((effect) => effect.kind === "summon" ? { ...effect, durationMs: 86_400_000 } : effect); case 492639: // Calm Heart, component 502908: 10 seconds of Fear immunity. return [{ kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-pair-calm-heart-fear-immunity", name: row.name, disposition: "buff", durationMs: 10_000, controlImmunities: ["fear"], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 0, }]; case 493046: // Sacred Protection, component 503290: +100 shield block for 20 seconds. return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-pair-sacred-protection-block", name: row.name, disposition: "buff", durationMs: 20_000, modifiers: [{ kind: "stat", stat: "block-chance", operation: "flat", value: 100 }], utilityTags: ["rom-sacred-protection"], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 172, miscValue: 100, triggerSpellId: 0, }]; case 493273: // Disregard Danger: redirect 40% of one hit; caster gains 20 Parry. return [ { kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-pair-disregard-danger-redirect", name: row.name, disposition: "buff", durationMs: 10_000, redirectDamagePercent: 0.4, utilityTags: ["rom-disregard-danger-one-hit"], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 143, miscValue: 40, triggerSpellId: 491614, }, { kind: "apply-aura", recipient: "caster", aura: { id: "rom-pair-disregard-danger-parry", name: `${row.name}: Parry`, disposition: "buff", durationMs: 10_000, modifiers: [{ kind: "stat", stat: "parry", operation: "flat", value: 20 }], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 22, miscValue: 20, triggerSpellId: 0, }, ]; case 491613: // Holy Chains only roots; source flag 67 is not three controls. return translated.filter((effect) => effect.kind === "apply-aura" && effect.control?.kind === "root"); default: return null; } } function hasTag(tags: readonly string[] | ReadonlySet, tag: string): boolean { return Array.isArray(tags) ? tags.includes(tag) : (tags as ReadonlySet).has(tag); } /** Apply passive pair upgrades when their parent skill is actually cast. */ export function romPriestKnightCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], passiveTags: readonly string[] | ReadonlySet, casterMaxHealth: number, targetTags: readonly string[] | ReadonlySet = [], friendlyTargetHealthPercent = 1, learnedPassiveSpellIds: readonly number[] = [], ): readonly AbilityEffect[] { if (ability.source !== "runewaker" || casterMaxHealth <= 0) return effects; const priestChampionEffects = romPriestChampionCastEffects(ability, effects, learnedPassiveSpellIds); if (priestChampionEffects !== effects) return priestChampionEffects; const bloodPunishmentEffects = romKnightScoutBloodPunishmentActivationEffects(ability, effects, passiveTags); if (bloodPunishmentEffects !== effects) return bloodPunishmentEffects; if (ability.dbcSpellId === 490276 && ability.classId === "rom-priest" && ability.secondaryClassId === undefined) { // Current CoA keeps Holy Void as Priest-primary and its Voidal Infection // lowers physical defense. Normalize that local stat alias to armor so the // generic aura resolver applies it to both character and NPC targets. Keep // magical-defense intact; NPCs use it for modeled resistances, but the // simulator has no Light/Holy resistance stat to reduce this spell's hit. return effects.map((effect) => effect.kind === "apply-aura" && effect.aura.id === "rom-priest-holy-void" ? { ...effect, aura: { ...effect.aura, modifiers: effect.aura.modifiers?.map((modifier) => ( modifier.kind === "stat" && modifier.stat === "physical-defense" ? { ...modifier, stat: "armor" } : modifier )), }, } : effect); } if (ability.dbcSpellId === 491588 && ability.classId === "rom-mage" && ability.secondaryClassId === "rom-priest" && hasTag(passiveTags, "rom-priest-mage-grand-dispel")) { // Purify's base tooltip/source row allows curse removal. Grand Dispel // broadens the same one-effect friendly cleanse to harmful status types. return effects.map((effect) => effect.kind === "dispel" && effect.mode === "cleanse" ? { ...effect, categories: ["magic", "curse", "poison", "disease"] } : effect); } if (ability.dbcSpellId === 490261 && learnedPassiveSpellIds.includes(493039) && friendlyTargetHealthPercent < 0.3) { // Saint's Blessing's active source components grant +30 healing points and // +1% healing power for 10 seconds. Put the aura before Heal so the // triggering cast itself receives the sourced bonus as well. return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-priest-saints-blessing-healing", name: "Saint's Blessing", disposition: "buff", durationMs: 10_000, modifiers: [ { kind: "healing", direction: "done", operation: "flat", value: 30 }, { kind: "healing", direction: "done", operation: "percent", value: 0.01 }, ], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 493039, }, ...effects]; } switch (ability.dbcSpellId) { case 493044: // Holy Power expires after three successful casts of Heal. return effects.map((effect) => effect.kind === "apply-aura" && effect.recipient === "caster" ? { ...effect, aura: { ...effect.aura, maxStacks: 3, initialStacks: 3, stackBehavior: "replace", utilityTags: [...(effect.aura.utilityTags ?? []), ROM_PRIEST_KNIGHT_HOLY_POWER_HEAL_USES_TAG], }, } : effect); case 490290: // Enhanced Grace of Life mirrors source Grace's +50 and +1% HP as MP. if (!hasTag(passiveTags, "rom-enhanced-grace-of-life")) return effects; return [...effects, { kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-pair-enhanced-grace-mana", name: "Enhanced Grace of Life: Mana", disposition: "buff", durationMs: 1_800_000, modifiers: [ { kind: "resource", resource: "mana", property: "maximum", operation: "flat", value: 50 }, { kind: "resource", resource: "mana", property: "maximum", operation: "percent", value: 0.01 }, ], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 8, miscValue: 50, triggerSpellId: 492355, }]; case 491147: // Life Link duplicates Urgent Heal's effective payload on the caster. if (!hasTag(passiveTags, "rom-life-link")) return effects; return [...effects, ...effects.filter((effect) => effect.kind === "heal") .map((effect) => ({ ...effect, recipient: "caster" as const }))]; case 490148: // Enhanced Holy Strike adds a conservative hybrid weapon bonus. if (!hasTag(passiveTags, "rom-enhanced-holy-strike")) return effects; return effects.map((effect) => effect.kind === "damage" ? { ...effect, coefficient: effect.coefficient * 1.1 } : effect); case 491354: // Weaken Weapon Strength debuffs targets hit by Heavenly Arrow. if (!hasTag(passiveTags, "rom-weaken-weapon-strength")) return effects; return [...effects, { kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-pair-heavenly-arrow-weaken", name: "Weaken Weapon Strength", disposition: "debuff", durationMs: 10_000, modifiers: [{ kind: "damage", direction: "dealt", school: "physical", operation: "percent", value: -0.1 }], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: -1, miscValue: -10, triggerSpellId: 490066, }]; case 491613: { // Evil Shackles halves duration on non-undead/demon targets. if (!hasTag(passiveTags, "rom-evil-shackles") || hasTag(targetTags, "rom-target-undead-demon")) return effects; return effects.map((effect) => effect.kind === "apply-aura" && effect.control?.kind === "root" ? { ...effect, aura: { ...effect.aura, durationMs: Math.max(1_000, Math.round((effect.aura.durationMs ?? 10_000) / 2)) } } : effect); } case 490273: // Source 499967 keeps Wave Armor's shield and adds heal component 623009. if (!hasTag(passiveTags, "rom-splendor-ripple")) return effects; return [...effects, { kind: "heal", recipient: "ability-target", coefficient: 0, basePoints: 106.25, pointsPerLevel: 21.25, sourceLevel: 1, }]; case 499966: // Devotion Halo refreshes Light Guidance while the halo is active. if (!hasTag(passiveTags, "rom-light-guidance")) return effects; return [...effects, { kind: "apply-aura", recipient: "caster", aura: { id: "rom-pair-light-guidance-healing", name: "Light Guidance", disposition: "buff", durationMs: 900_000, modifiers: [{ kind: "healing", direction: "done", operation: "percent", value: 0.05 }], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 149, miscValue: 5, triggerSpellId: 623015, }]; case 490212: // Lightning Rod adds source child 502907 only while Bone Chill is on the target. if (!hasTag(passiveTags, "rom-priest-mage-lightning-rod") || !hasTag(targetTags, "rom-491165-501548-0")) return effects; return [...effects, { kind: "apply-aura", recipient: "ability-target", aura: { id: "rom-priest-mage-lightning-rod-shock", name: "Electric Shock", disposition: "debuff", durationMs: 20_000, modifiers: [ { kind: "stat", stat: "resistance:frost", operation: "flat", value: -50 }, { kind: "stat", stat: "resistance:nature", operation: "flat", value: -50 }, ], utilityTags: ["rom-priest-mage-lightning-rod-shock"], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 502907, }]; default: return effects; } } /** Select the pair-specific payload branches that depend on target state. */ export function romPriestKnightConditionalCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], sealStacks: number, targetIsCaster: boolean, ): readonly AbilityEffect[] { if (ability.source !== "runewaker" || ability.classId !== "rom-knight" || ability.secondaryClassId !== "rom-priest") return effects; if (ability.dbcSpellId === 490194) { const baseHit = effects.find((effect) => effect.kind === "damage"); if (!baseHit) return effects; // The generated row carries a placeholder heal with no source-backed // amount; only the Holy Seal components grant healing. const nonDamage = effects.filter((effect) => effect.kind !== "damage" && effect.kind !== "heal"); const seals = Math.min(4, Math.max(0, Math.trunc(sealStacks))); if (seals === 0) return [baseHit, ...nonDamage]; const healPercent = Math.min(8, seals * 2) / 100; return [ baseHit, ...nonDamage, { kind: "heal", coefficient: 0, percentMaxHealth: healPercent, recipient: "caster" }, { kind: "heal", coefficient: 0, percentMaxHealth: healPercent, scope: "party", center: "caster", maximumTargets: 1, excludeCaster: true, }, { kind: "combo", amount: -seals }, ]; } if (ability.dbcSpellId === 491640) { const selectedThreatPolarity = targetIsCaster ? 1 : -1; const hasThreatPayload = effects.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "threat")); if (!hasThreatPayload) return effects; return effects.map((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "threat") ? { ...effect, aura: { ...effect.aura, modifiers: effect.aura.modifiers?.map((modifier) => modifier.kind === "stat" && modifier.stat === "threat" ? { ...modifier, value: Math.abs(modifier.value) * selectedThreatPolarity } : modifier), }, } : effect); } return effects; } /** Source skill 499966 is granted by the level-70 Priest/Knight elite pair. */ export function romPriestKnightAdditionalAbilities( catalog: readonly AbilityDefinition[], ): readonly AbilityDefinition[] { const lightGuidance = catalog.find((ability) => ability.dbcSpellId === 499957 && ability.classId === "rom-priest" && ability.secondaryClassId === "rom-knight"); if (!lightGuidance) return []; const effects: readonly AbilityEffect[] = [{ kind: "apply-aura", recipient: "caster", scope: "party", aura: { id: "rom-pair-devotion-halo", name: "Devotion Halo", disposition: "buff", durationMs: 900_000, modifiers: [{ kind: "stat", stat: "wisdom", operation: "percent", value: 0.05 }], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 164, miscValue: 5, triggerSpellId: 623016, }, { kind: "apply-aura", recipient: "caster", scope: "party", excludeCaster: true, aura: { id: "rom-pair-devotion-halo-mana", name: "Devotion Halo Mana", disposition: "buff", durationMs: 900_000, modifiers: [{ kind: "resource", resource: "mana", property: "cost", operation: "percent", value: -0.06 }], dispelCategory: "magic", }, sourceEffectType: -1, sourceAuraType: 164, miscValue: -6, triggerSpellId: 623016, }]; return [{ id: "rom-priest-rom-knight-elite-70-devotion-halo", classId: "rom-priest", secondaryClassId: "rom-knight", dbcSpellId: 499966, name: "Devotion Halo", unlockLevel: 70, icon: lightGuidance.icon, sigil: "DH", description: "Raises party Wisdom by 5% and lowers party mana costs by 6% for 15 minutes.", target: "self", range: { min: 0, max: 5 }, radius: 20, castMode: "instant", castTimeMs: 0, cooldownMs: 0, gcdMs: 1_500, cost: { resource: "mana", amount: 0 }, costs: [], effects, passive: false, source: "runewaker", sourceUnlockLevel: 70, romSkillGroup: "elite", }]; }