Files
healer-man/src/game/romWarlockOverrides.ts
T
2026-09-28 22:46:45 -04:00

196 lines
7.8 KiB
TypeScript

import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
import type { RomGeneratedAbilityLike } from "./romAbilitySemantics";
export const ROM_WARLOCK_STATE_TAGS = Object.freeze({
heartFence: "rom-warlock-state-501577",
perceptionExtraction: "rom-warlock-state-497960",
soulPain: "rom-warlock-state-497964",
soulPoisonedFang: "rom-warlock-state-498670",
} as const);
const CRIME_AND_PUNISHMENT_STATE_TAGS = [
ROM_WARLOCK_STATE_TAGS.perceptionExtraction,
ROM_WARLOCK_STATE_TAGS.soulPain,
ROM_WARLOCK_STATE_TAGS.soulPoisonedFang,
] as const;
function appendAuraTag(effect: AbilityEffect, tag: string): AbilityEffect {
if (effect.kind !== "apply-aura") return effect;
return {
...effect,
aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), tag] },
};
}
/** Source-specific state markers and recipient mapping for the Warlock's paired skills. */
export function overrideRomWarlockEffects(
row: RomGeneratedAbilityLike,
translated: readonly AbilityEffect[],
): readonly AbilityEffect[] | null {
if (row.classId !== "rom-warlock") return null;
if (row.sourceSkillId === 498549) {
// Defense Net's active component supplies the 5-second magic mitigation;
// the source tooltip also describes immunity to Helpless, Stun, Root, and
// Slow. In this runtime, Helpless maps to fear control.
return translated.map((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 142
? {
...effect,
aura: {
...effect.aura,
controlImmunities: ["fear", "stun", "root", "slow"],
},
}
: effect);
}
if (row.sourceSkillId === 498710 || row.sourceSkillId === 498662) {
const targetAbilityName = row.sourceSkillId === 498710 ? "Flaming Heart Strike" : "Rising Tide";
return [
...translated.filter((effect) => effect.kind !== "cooldown-reset"),
{ kind: "cooldown-reset", mode: "reset", abilityName: targetAbilityName },
];
}
if ([498634, 498648, 498668].includes(row.sourceSkillId ?? 0)) {
// These passive tooltips describe event-gated resets. Keep their cooldown
// riders out of the generic cast path until their source events are wired.
return translated.filter((effect) => effect.kind !== "cooldown-reset");
}
if (row.sourceSkillId === 498695) {
let selectedTargetThreatAura = false;
const selectedTargetEffects: AbilityEffect[] = translated.flatMap((effect): AbilityEffect[] => {
if (effect.kind !== "apply-aura") return [effect];
const threatModifiers = effect.aura.modifiers?.filter((modifier) => (
modifier.kind === "stat" && modifier.stat === "threat"
)) ?? [];
const threatModifier = threatModifiers[0];
if (!threatModifier || threatModifier.kind !== "stat") return [effect];
if (threatModifier.value > 0) {
// 621654 repeats the +5% modifier on a hidden script component; the tooltip
// describes one buff on the selected friendly target.
if (selectedTargetThreatAura) return [];
selectedTargetThreatAura = true;
return [{ ...effect, recipient: "ability-target" }];
}
if (threatModifier.value < 0) {
return [{ ...effect, recipient: "caster", scope: "party" }];
}
return [effect];
});
const reductionComponent = row.source?.components?.find((component) => component.id === 621433);
const reductionModifier = reductionComponent?.abilityModifiers?.find((modifier) => modifier.type === 138);
if (!reductionComponent || !reductionModifier) return selectedTargetEffects;
const partyReductionAura: AbilityEffect = {
kind: "apply-aura",
aura: {
id: "rom-warlock-liquidation-party-threat-reduction",
name: `${row.name}: party threat reduction`,
disposition: "buff",
durationMs: Math.max(0, Math.round((reductionComponent.effectTime ?? 10) * 1_000)),
modifiers: [{
kind: "stat",
stat: "threat",
operation: "percent",
value: reductionModifier.value / 100,
}],
},
recipient: "caster",
scope: "party",
sourceEffectType: -1,
sourceAuraType: 138,
miscValue: reductionModifier.value,
triggerSpellId: row.sourceSkillId ?? 0,
};
return [...selectedTargetEffects, partyReductionAura];
}
const stateTag = row.sourceSkillId === 497960
? ROM_WARLOCK_STATE_TAGS.perceptionExtraction
: row.sourceSkillId === 497964
? ROM_WARLOCK_STATE_TAGS.soulPain
: row.sourceSkillId === 498670
? ROM_WARLOCK_STATE_TAGS.soulPoisonedFang
: null;
if (stateTag) {
return translated.map((effect) => effect.kind === "dot"
? { ...effect, tag: stateTag }
: appendAuraTag(effect, stateTag));
}
if (row.source?.components?.some((component) => component.id === 501577)) {
return translated.map((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 226
? appendAuraTag(effect, ROM_WARLOCK_STATE_TAGS.heartFence)
: effect);
}
return null;
}
function scaleDamage(effect: AbilityEffect, factor: number): AbilityEffect {
if (effect.kind !== "damage") return effect;
return {
...effect,
coefficient: effect.coefficient * factor,
...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * factor }),
...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints * factor }),
...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel * factor }),
...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient * factor }),
...(effect.bonusFlat === undefined ? {} : { bonusFlat: effect.bonusFlat * factor }),
};
}
/** Add only the source-described conditional riders for the Warlock primary build. */
export function romWarlockCastEffects(
ability: AbilityDefinition,
effects: readonly AbilityEffect[],
targetTags: readonly string[],
learnedPassiveSpellIds: readonly number[] = [],
): readonly AbilityEffect[] {
if (ability.source !== "runewaker" || ability.classId !== "rom-warlock") return effects;
if (ability.dbcSpellId === 498549
&& learnedPassiveSpellIds.includes(498648)) {
// Mind Shield (498648) reduces Defense Net's sourced five-second aura by one second.
return effects.map((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 142
? {
...effect,
aura: { ...effect.aura, durationMs: Math.max(0, (effect.aura.durationMs ?? 0) - 1_000) },
}
: effect);
}
if (ability.dbcSpellId === 498646 && ability.secondaryClassId === "rom-warrior"
&& targetTags.includes(ROM_WARLOCK_STATE_TAGS.heartFence)) {
return [...effects, { kind: "resource", amount: 100, percentage: true, resource: "rage" }];
}
if (ability.dbcSpellId === 498700 && ability.secondaryClassId === "rom-rogue") {
const matchedStates = CRIME_AND_PUNISHMENT_STATE_TAGS.filter((tag) => targetTags.includes(tag));
const additionalStates = Math.max(0, matchedStates.length - 1);
return additionalStates > 0
? effects.map((effect) => scaleDamage(effect, 1 + additionalStates * 0.25))
: effects;
}
return effects;
}
/** Return state tags whose source durations are refreshed by this successful cast. */
export function romWarlockStateRefreshTags(
ability: AbilityDefinition,
targetTags: readonly string[],
): readonly string[] {
if (ability.source !== "runewaker" || ability.classId !== "rom-warlock") return [];
if (ability.dbcSpellId === 498646 && ability.secondaryClassId === "rom-warrior") {
return targetTags.includes(ROM_WARLOCK_STATE_TAGS.heartFence)
? [ROM_WARLOCK_STATE_TAGS.heartFence]
: [];
}
if (ability.dbcSpellId === 498700 && ability.secondaryClassId === "rom-rogue") {
return CRIME_AND_PUNISHMENT_STATE_TAGS.filter((tag) => targetTags.includes(tag));
}
return [];
}