import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import { weaponDps, type WeaponStats } from "./itemStats"; import { runewakerOrgPower } from "./runewakerCombatFormula"; export const ROM_WARDEN_BRIAR_SHIELD_TAG = "rom-briar-shield"; export const ROM_WARDEN_WILL_ATTACK_TAG = "rom-will-attack"; export const ROM_WARDEN_DAMAGE_TRANSFER_TAG = "rom-warden-damage-transfer"; export const ROM_WARDEN_TOUGH_OAK_TAG = "rom-warden-tough-oak"; export const ROM_WARDEN_OAK_BLESSING_TAG = "rom-warden-oak-blessing"; export const ROM_WARDEN_DAMAGE_TRANSFER_FRACTION = 0.3; /** RuneWaker component 503959's level-scaled physical rebound coefficient. */ export function romWardenBriarShieldReflectFraction(characterLevel: number): number { return Math.abs(runewakerOrgPower(-0.01, characterLevel, 8)); } /** The local tooltip defines the rebound basis as main-hand weapon DPS. */ export function romWardenBriarShieldReflectDamage( characterLevel: number, mainHandWeapon: WeaponStats | null, ): number { return mainHandWeapon ? Math.max(0, Math.round(weaponDps(mainHandWeapon) * romWardenBriarShieldReflectFraction(characterLevel))) : 0; } export function romWardenBriarShieldShouldReflect(input: { readonly auraActive: boolean; readonly landed: boolean; readonly school: string; readonly attackKind: "melee" | "ranged" | "spell"; readonly withinRange: boolean; }): boolean { return input.auraActive && input.landed && input.school === "physical" && input.attackKind !== "spell" && input.withinRange; } const ROM_WARDEN_TOUGH_OAK_COOLDOWN_SPELLS = new Set([493382, 493385]); const classPair = (ability: AbilityDefinition, primary: string, secondary: string): boolean => ( ability.classId === primary && ability.secondaryClassId === secondary ); function casterAura( ability: AbilityDefinition, id: string, name: string, durationMs: number | null, options: Pick["aura"], "controlImmunities" | "utilityTags">, ): Extract { return { kind: "apply-aura", recipient: "caster", aura: { id, name, disposition: "buff", durationMs, ...options, }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: ability.dbcSpellId, }; } /** Select source-described Warden and Warden-linked class payloads. */ export function romWardenCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], naturePower: number, passiveTags: readonly string[] = [], ): readonly AbilityEffect[] { if (ability.source !== "runewaker") return effects; if (ability.classId === "rom-warden" && ability.dbcSpellId !== undefined && ROM_WARDEN_TOUGH_OAK_COOLDOWN_SPELLS.has(ability.dbcSpellId) && passiveTags.includes(ROM_WARDEN_TOUGH_OAK_TAG)) { return [...effects, { kind: "cooldown-reset", mode: "reduce", abilityName: ability.name, amountMs: 1_000, }]; } if (ability.classId === "rom-warden" && ability.dbcSpellId === 491668 && passiveTags.includes(ROM_WARDEN_OAK_BLESSING_TAG)) { // Warden/Mage's local Oak Blessing component 620679 is conditional on // Heart of the Oak: +5% physical attack and 1% maximum HP every 3s for // 60s. The generated passive itself is only a marker so it cannot apply // this rider outside the parent buff's lifetime. return [...effects, { kind: "apply-aura", recipient: "caster", aura: { id: "rom-warden-oak-blessing-attack", name: "Oak Blessing", disposition: "buff", durationMs: 60_000, modifiers: [{ kind: "stat", stat: "attack-power", operation: "percent", value: 0.05 }], }, sourceEffectType: -1, sourceAuraType: 134, miscValue: 5, triggerSpellId: 491668, }, { kind: "hot", coefficient: 0, percentMaxHealth: 0.01, ticks: 20, intervalMs: 3_000, }, ]; } if (ability.dbcSpellId === 493391 && ability.classId === "rom-warden") { // The source row includes Will Attack's component, but it belongs on the // target only when the Warden has learned that passive. const reduction = effects.find((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 173); const baseEffects = effects.filter((effect) => !(effect.kind === "apply-aura" && effect.sourceAuraType === 173)); if (!passiveTags.includes(ROM_WARDEN_WILL_ATTACK_TAG) || !reduction || reduction.kind !== "apply-aura") { return baseEffects; } return [...baseEffects, { ...reduction, recipient: "ability-target", aura: { ...reduction.aura, id: "rom-warden-will-attack", name: "Will Attack", disposition: "debuff", utilityTags: [...(reduction.aura.utilityTags ?? []), ROM_WARDEN_WILL_ATTACK_TAG], }, }]; } if (ability.dbcSpellId === 493399 && ability.classId === "rom-warden") { return effects.map((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "damage" && modifier.direction === "taken" && modifier.operation === "percent" && modifier.value === -ROM_WARDEN_DAMAGE_TRANSFER_FRACTION) ? { ...effect, aura: { ...effect.aura, // Component 503942 is the executable modifier: RuneWaker encodes // ten seconds here. Component 503643 is tooltip-only and says 15s. durationMs: 10_000, utilityTags: [...(effect.aura.utilityTags ?? []), ROM_WARDEN_DAMAGE_TRANSFER_TAG], }, } : effect); } if (ability.dbcSpellId === 494043 && classPair(ability, "rom-rogue", "rom-warden")) { // The source describes a chance on the second hit but contains no proc rate. // Keep the guaranteed base strike and do not turn an unknown chance into 100%. let keptBaseHit = false; return effects.filter((effect) => { if (effect.kind !== "damage") return true; if (keptBaseHit) return false; keptBaseHit = true; return true; }); } if (ability.dbcSpellId === 499495 && classPair(ability, "rom-warrior", "rom-warden")) { return [ casterAura(ability, "rom-warden-awakening-root-slow-immunity", ability.name, 10_000, { controlImmunities: ["root", "slow"], utilityTags: ["rom-awakening-of-the-wild-immunity"], }), casterAura(ability, "rom-warden-awakening-next-critical", ability.name, null, { utilityTags: ["next-offensive-critical", "rom-awakening-of-the-wild-next-critical"], }), ]; } if (ability.dbcSpellId === 494323 && classPair(ability, "rom-druid", "rom-warden")) { const heals = effects.filter((effect): effect is Extract => effect.kind === "heal"); if (!heals.length) return effects; if (naturePower < 1) return [heals[0]!]; return [ ...heals.slice(0, 2), { kind: "resource-drain", amount: 1, burn: false, resource: "nature-power", recipient: "caster" }, ]; } if (ability.dbcSpellId === 499936 && classPair(ability, "rom-druid", "rom-warden")) { const points = Math.max(0, Math.floor(naturePower)); if (points < 1) return []; const heal = effects.find((effect): effect is Extract => effect.kind === "heal"); if (!heal) return effects; return [ { ...heal, coefficient: 0, percentMaxHealth: Math.min(1, points * 0.1), basePoints: 0, dieSides: 0, pointsPerLevel: 0, coefficientPerLevel: 0, bonusPowerCoefficient: 0, }, { kind: "resource-drain", amount: points, burn: false, resource: "nature-power", recipient: "caster" }, ]; } if (ability.dbcSpellId === 494372 && classPair(ability, "rom-druid", "rom-warden")) { return effects.map((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.length ? { ...effect, scope: "party" } : effect); } return effects; }