Files
healer-man/src/game/romKnightOverrides.ts
T

429 lines
15 KiB
TypeScript

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<AbilityDefinition, "source" | "classId" | "secondaryClassId" | "dbcSpellId">;
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<AbilityDefinition, "source" | "classId" | "dbcSpellId">,
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<AbilityDefinition, "source" | "classId" | "dbcSpellId">,
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<string>,
tag: string,
): boolean {
return Array.isArray(passiveTags)
? passiveTags.includes(tag)
: (passiveTags as ReadonlySet<string>).has(tag);
}
/** CoA 6-1: Deadly Wound reduces healing received by 30%; player targets last 5s. */
export function romKnightDeadlyWhirlwindAura(targetIsPlayer: boolean): Extract<AbilityEffect, { kind: "apply-aura" }> {
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<AbilityEffect, { kind: "apply-aura" }> | 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<AbilityDefinition, "source" | "classId" | "dbcSpellId">,
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<string> = [],
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;
}