Files
healer-man/src/game/romChampionOverrides.ts
T
2026-09-30 09:57:13 -04:00

311 lines
13 KiB
TypeScript

import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
import type { AuraDefinition } from "./combatAuras";
import type { RomGeneratedAbilityLike } from "./romAbilitySemantics";
export const ROM_CHAMPION_FORGE_STACK_AURA: AuraDefinition = {
id: "rom-champion-forge-stack",
name: "Forge",
description: "Each normal attack hit grants 2% physical attack for 8 seconds, stacking up to 5 times.",
disposition: "buff",
durationMs: 8_000,
maxStacks: 5,
stackBehavior: "add",
modifiers: [{
kind: "stat",
stat: "attack-power",
operation: "percent",
value: 0.02,
perStack: true,
}],
};
export const ROM_CHAMPION_RUNE_OVERLOAD_TAG = "rom-champion-rune-overload";
export const ROM_CHAMPION_RUNE_OVERLOAD_RAGE_PER_SECOND = 15;
export const ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID = "rom-champion-soul-forge-critical";
export const ROM_CHAMPION_SOUL_FORGE_PROFILE = Object.freeze({
sourceSpellId: 498652,
criticalAuraComponentId: 621339,
criticalAuraDurationMs: 300_000,
darkPowerComponentId: 621341,
trigger: "crit" as const,
chance: 1,
darkMagicAuraDurationMs: 10_000,
darkMagicBonusPercent: 0.1,
darkMagicSchool: "shadow" as const,
});
export const ROM_CHAMPION_WAY_OF_FRENZY_PROC_ACTION_ID = "rom-champion-way-of-frenzy-hit";
export const ROM_CHAMPION_WAY_OF_FRENZY_PROFILE = Object.freeze({
sourceSpellId: 498720,
sourceComponentId: 621458,
triggerSpellId: 498715,
triggerAbilityId: "rom-champion-rom-priest-elite-20",
durationMs: 60_000,
maxStacks: 5,
staminaPercentPerStack: 0.02,
});
const ROM_CHAMPION_FORGE_ACTIVE_AURA: AuraDefinition = {
id: "rom-champion-forge-active",
name: "Forge",
description: "For 5 minutes, normal attack hits build Forge stacks and restore Rage.",
disposition: "buff",
durationMs: 300_000,
utilityTags: ["rom-champion-forge-active"],
procs: [{
id: "rom-champion-forge-normal-hit",
triggers: ["hit", "crit"],
attackKinds: ["melee"],
chance: 1,
action: { kind: "custom", id: "rom-champion-forge-hit" },
}],
};
/** Replace the source component's unconditional stat aura with its described hit trigger. */
export function overrideRomChampionEffects(
row: RomGeneratedAbilityLike,
_translated: readonly AbilityEffect[],
): readonly AbilityEffect[] | null {
if (row.classId !== "rom-champion" || row.secondaryClassId !== "rom-priest"
|| row.sourceSkillId !== ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId) return null;
const component = row.source?.components?.find((entry) => (
entry.id === ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceComponentId
));
const staminaModifier = component?.abilityModifiers?.find((modifier) => modifier.type === 162);
const durationMs = Math.max(0, Math.round((component?.effectTime ?? 0) * 1_000));
if (!component || !staminaModifier || staminaModifier.value <= 0 || durationMs <= 0) return [];
return [{
kind: "apply-aura",
aura: {
id: "rom-champion-way-of-frenzy-passive",
name: row.name,
disposition: "buff",
durationMs: null,
hideFromAuraStrip: true,
procs: [{
id: "rom-champion-way-of-frenzy-divine-vengeance",
triggers: ["hit", "crit"],
abilityIds: [ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerAbilityId],
chance: 1,
action: {
kind: "custom",
id: ROM_CHAMPION_WAY_OF_FRENZY_PROC_ACTION_ID,
data: {
sourceSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId,
sourceComponentId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceComponentId,
triggerSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerSpellId,
durationMs,
maxStacks: 5,
staminaPercentPerStack: staminaModifier.value / 100,
},
},
}],
},
recipient: "caster",
sourceEffectType: -1,
sourceAuraType: -1,
miscValue: 0,
triggerSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId,
}];
}
function scaleDamage(effect: Extract<AbilityEffect, { kind: "damage" }>, factor: number): AbilityEffect {
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 }),
};
}
function casterHealFromDamage(effect: Extract<AbilityEffect, { kind: "damage" }>): AbilityEffect {
return {
kind: "heal",
coefficient: effect.coefficient,
...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints }),
...(effect.dieSides === undefined ? {} : { dieSides: effect.dieSides }),
...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel }),
...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel }),
...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient }),
...(effect.attackPowerCoefficient === undefined ? {} : { attackPowerCoefficient: effect.attackPowerCoefficient }),
...(effect.spiritCoefficient === undefined ? {} : { spiritCoefficient: effect.spiritCoefficient }),
...(effect.sourceLevel === undefined ? {} : { sourceLevel: effect.sourceLevel }),
...(effect.maxScalingLevel === undefined ? {} : { maxScalingLevel: effect.maxScalingLevel }),
recipient: "caster",
};
}
function soulForgeMysteryEffects(effects: readonly AbilityEffect[]): readonly AbilityEffect[] {
const critAuraIndex = effects.findIndex((effect) => effect.kind === "apply-aura"
&& effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.criticalAuraComponentId}-`));
const darkAuraIndex = effects.findIndex((effect) => effect.kind === "apply-aura"
&& effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.darkPowerComponentId}-`));
const critAuraEffect = effects[critAuraIndex];
const darkAuraEffect = effects[darkAuraIndex];
if (critAuraIndex < 0 || darkAuraIndex < 0
|| critAuraEffect?.kind !== "apply-aura" || darkAuraEffect?.kind !== "apply-aura") return effects;
const darkModifier = darkAuraEffect.aura.modifiers?.find((modifier) => modifier.kind === "damage"
&& modifier.direction === "dealt" && modifier.school === ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicSchool
&& modifier.operation === "percent");
if (darkModifier?.kind !== "damage") return effects;
const output: AbilityEffect[] = [];
effects.forEach((effect, index) => {
if (index === darkAuraIndex) return;
if (index === critAuraIndex && effect.kind === "apply-aura") {
output.push({
...effect,
aura: {
...effect.aura,
procs: [
...(effect.aura.procs ?? []),
{
id: "rom-champion-soul-forge-dark-magic-on-crit",
triggers: ROM_CHAMPION_SOUL_FORGE_PROFILE.trigger,
chance: ROM_CHAMPION_SOUL_FORGE_PROFILE.chance,
action: {
kind: "custom",
id: ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID,
data: {
sourceSpellId: ROM_CHAMPION_SOUL_FORGE_PROFILE.sourceSpellId,
sourceComponentId: ROM_CHAMPION_SOUL_FORGE_PROFILE.darkPowerComponentId,
auraId: "rom-champion-soul-forge-dark-magic",
auraName: darkAuraEffect.aura.name,
durationMs: darkAuraEffect.aura.durationMs ?? ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicAuraDurationMs,
disposition: "buff",
maxStacks: 1,
stackBehavior: "refresh",
target: "aura-source",
modifierKind: "damage",
modifierDirection: "dealt",
modifierSchool: ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicSchool,
modifierOperation: darkModifier.operation,
modifierValue: darkModifier.value,
},
},
},
],
},
});
return;
}
output.push(effect);
});
return output;
}
/** Apply source-backed Champion cast branches that need runtime state. */
export function romChampionCastEffects(
ability: AbilityDefinition,
effects: readonly AbilityEffect[],
targetHealthPercent: number,
casterMaxHealth: number,
): readonly AbilityEffect[] {
if (ability.source !== "runewaker" || ability.classId !== "rom-champion") return effects;
switch (ability.dbcSpellId) {
case 498652: // Soul Forge Mystery adds its 10-second dark magic buff on a critical attack.
if (ability.secondaryClassId !== "rom-warlock") return effects;
return soulForgeMysteryEffects(effects);
case 498595: { // Rune Siphon's positive component 621264 restores the caster's HP.
if (ability.secondaryClassId !== "rom-warlock") return effects;
let healIndex = -1;
for (let index = effects.length - 1; index >= 0; index -= 1) {
if (effects[index]?.kind === "damage") {
healIndex = index;
break;
}
}
if (healIndex < 0) return effects;
return effects.flatMap((effect, index): AbilityEffect[] => {
if (index === healIndex && effect.kind === "damage") return [casterHealFromDamage(effect)];
if (effect.kind === "heal" && effect.coefficient === 0 && effect.basePoints === undefined
&& effect.pointsPerLevel === undefined && effect.percentMaxHealth === undefined) return [];
return [effect];
});
}
case 498529: { // Rune Pulse restores Rage on a landed hit (component 622327).
const restoreAura = effects.find((effect) => effect.kind === "apply-aura"
&& effect.aura.id.includes("-622327-")
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "resource"
&& modifier.resource === "rage" && modifier.property === "regeneration"));
if (restoreAura?.kind !== "apply-aura") return effects;
const rageModifier = restoreAura.aura.modifiers?.find((modifier) => modifier.kind === "resource"
&& modifier.resource === "rage" && modifier.property === "regeneration");
const ragePerHit = rageModifier
? Math.max(0, Math.round(rageModifier.value * (restoreAura.aura.durationMs ?? 0) / 1_000))
: 0;
const damageIndex = effects.findIndex((effect) => effect.kind === "damage");
if (ragePerHit <= 0 || damageIndex < 0) return effects;
const output: AbilityEffect[] = [];
effects.forEach((effect, index) => {
if (index === damageIndex && effect.kind === "damage") {
output.push({ ...effect, onDamageResource: { resource: "rage", amount: ragePerHit } });
} else if (effect !== restoreAura) {
output.push(effect);
}
});
return output;
}
case 498584: // Champion/Warrior Determination Rune is a 30%-of-max-HP caster shield.
if (ability.secondaryClassId !== "rom-warrior") return effects;
return effects.map((effect) => effect.kind === "apply-aura" && effect.aura.absorbs?.length
? {
...effect,
aura: {
...effect.aura,
absorbs: effect.aura.absorbs.map((absorb) => ({
...absorb,
amount: Math.max(1, Math.round(Math.max(0, casterMaxHealth) * absorb.amount / 100)),
})),
},
}
: effect);
case 498523: // Rank 1 Forge's guaranteed on-normal-attack trigger is child skill 498524.
return [{
kind: "apply-aura",
aura: ROM_CHAMPION_FORGE_ACTIVE_AURA,
recipient: "caster",
sourceEffectType: 0,
sourceAuraType: 0,
miscValue: 0,
triggerSpellId: 498524,
}];
case 498572: // Rune Overload ends when its Rage drain reaches zero.
return effects.map((effect) => effect.kind === "apply-aura"
? {
...effect,
aura: {
...effect.aura,
utilityTags: [...(effect.aura.utilityTags ?? []), ROM_CHAMPION_RUNE_OVERLOAD_TAG],
},
}
: effect);
case 498531: // Fearless Blow's +30% hit applies only below 30% target health.
return effects.flatMap((effect) => {
if (effect.kind === "apply-aura" && effect.aura.id.includes("-621179-")) return [];
if (effect.kind === "apply-aura" && effect.aura.id.includes("-621183-")) {
return [{ ...effect, recipient: "caster" as const }];
}
if (effect.kind === "damage" && targetHealthPercent < 0.3) return [scaleDamage(effect, 1.3)];
return [effect];
});
case 498535: // Chain Drive's proc chance and damage bonus are absent from the source payload.
return effects.filter((effect) => effect.kind !== "cooldown-reset");
case 498599: // Dark Energy Strike's cooldown proc chance is absent from the source payload.
if (ability.secondaryClassId !== "rom-warlock") return effects;
return effects.filter((effect) => effect.kind !== "cooldown-reset");
case 498651: // Heart Collection Rune explicitly resets Rune Siphon.
if (ability.secondaryClassId !== "rom-warlock") return effects;
return effects.map((effect) => effect.kind === "cooldown-reset"
? { ...effect, abilityName: "Rune Siphon" }
: effect);
default:
return effects;
}
}