311 lines
13 KiB
TypeScript
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;
|
|
}
|
|
}
|