Files
healer-man/src/game/romWardenOverrides.ts
T
2026-10-02 10:38:12 -04:00

227 lines
8.0 KiB
TypeScript

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<Extract<AbilityEffect, { kind: "apply-aura" }>["aura"], "controlImmunities" | "utilityTags">,
): Extract<AbilityEffect, { kind: "apply-aura" }> {
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<AbilityEffect, { kind: "heal" }> => 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<AbilityEffect, { kind: "heal" }> => 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;
}