312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
|
|
import type { RomGeneratedAbilityLike } from "./romAbilitySemantics";
|
|
|
|
/** The absorb amounts in Electrostatic Charge's source row are HP percentages. */
|
|
export const ROM_ELECTROSTATIC_PERCENT_ABSORB_TAG = "rom-electrostatic-percent-max-health";
|
|
|
|
export const ROM_MAGE_WARLOCK_PASSIVE_TAGS = Object.freeze({
|
|
eruption: "rom-mage-eruption",
|
|
controlFlame: "rom-mage-druid-control-flame",
|
|
controlFlameCooldown: "rom-mage-druid-control-flame-cooldown",
|
|
flameFollower: "rom-mage-warden-flame-follower",
|
|
industryFire: "rom-mage-warlock-industry-fire",
|
|
lightningDisplay: "rom-mage-warlock-lightning-display",
|
|
elementsOfPride: "rom-mage-warlock-elements-of-pride",
|
|
soulSpeed: "rom-mage-warlock-soul-speed",
|
|
elementalExtraction: "rom-mage-warlock-elemental-extraction",
|
|
} as const);
|
|
|
|
export const ROM_SOUL_STEPPING_CAST_TAG = "rom-mage-warlock-soul-stepping-cast";
|
|
export const ROM_BREATH_ERASE_THREAT_TAG = "rom-mage-warlock-breath-erase-threat";
|
|
export const ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG = "rom-fire-lightning-burst-reset-guard";
|
|
export const ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS = 6_000;
|
|
|
|
// Hidden replacement skill 498746: 621504 is the initial Fire hit, and
|
|
// 621503 is its 24-second Fire DoT. These values come from MagicObject.db.
|
|
const INDUSTRY_FIRE_HIT_BASE = 140;
|
|
const INDUSTRY_FIRE_HIT_PER_LEVEL = 40;
|
|
const INDUSTRY_FIRE_DOT_BASE = 23.4;
|
|
const INDUSTRY_FIRE_DOT_PER_LEVEL = 10.4;
|
|
export const ROM_COA_MAGE_ERUPTION_BASE_PROC_CHANCE = 0.3;
|
|
|
|
function scaledDamage(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 }),
|
|
};
|
|
}
|
|
|
|
function controlFlameTriggeredAuras(): readonly AbilityEffect[] {
|
|
return [
|
|
{
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: {
|
|
id: "rom-mage-druid-control-flame-buff",
|
|
name: "Control Flame",
|
|
disposition: "buff",
|
|
durationMs: 12_000,
|
|
modifiers: [{ kind: "stat", stat: "magical-attack", operation: "percent", value: 0.08 }],
|
|
},
|
|
sourceEffectType: -1,
|
|
sourceAuraType: 171,
|
|
miscValue: 8,
|
|
triggerSpellId: 494054,
|
|
},
|
|
{
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: {
|
|
id: "rom-mage-druid-control-flame-cooldown",
|
|
name: "Control Flame cooldown",
|
|
disposition: "buff",
|
|
durationMs: 20_000,
|
|
utilityTags: [ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlameCooldown],
|
|
},
|
|
sourceEffectType: -1,
|
|
sourceAuraType: -1,
|
|
miscValue: 0,
|
|
triggerSpellId: 494078,
|
|
},
|
|
];
|
|
}
|
|
|
|
function stateAura(id: string, tag: string, durationMs: number | null): AbilityEffect {
|
|
return {
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: {
|
|
id: `rom-mage-warlock-state-${id}`,
|
|
name: "Mage/Warlock class state",
|
|
disposition: "buff",
|
|
durationMs,
|
|
maxStacks: 1,
|
|
stackBehavior: "refresh",
|
|
utilityTags: [tag],
|
|
},
|
|
sourceEffectType: -1,
|
|
sourceAuraType: -1,
|
|
miscValue: 0,
|
|
triggerSpellId: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Source-specific mappings for the accepted Mage/Warlock build. These auras
|
|
* are state markers; the combat runtime applies the conditional skill effects
|
|
* only when the named source spells execute or critically hit their target.
|
|
*/
|
|
export function overrideRomMageWarlockEffects(
|
|
row: RomGeneratedAbilityLike,
|
|
translated: readonly AbilityEffect[],
|
|
): readonly AbilityEffect[] | null {
|
|
if (row.sourceSkillId === 490221) return [stateAura("eruption", ROM_MAGE_WARLOCK_PASSIVE_TAGS.eruption, null)];
|
|
if (row.sourceSkillId === 494054) return [stateAura("control-flame", ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlame, null)];
|
|
if (row.sourceSkillId === 494051) return [stateAura("flame-follower", ROM_MAGE_WARLOCK_PASSIVE_TAGS.flameFollower, null)];
|
|
|
|
if (row.sourceSkillId === 491156) {
|
|
return translated.map((effect) => {
|
|
if (effect.kind !== "apply-aura" || !effect.aura.absorbs?.length) return effect;
|
|
return {
|
|
...effect,
|
|
aura: {
|
|
...effect.aura,
|
|
utilityTags: [...(effect.aura.utilityTags ?? []), ROM_ELECTROSTATIC_PERCENT_ABSORB_TAG],
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
if (row.sourceSkillId === 498750) {
|
|
return translated.map((effect) => effect.kind === "apply-aura" ? {
|
|
...effect,
|
|
aura: {
|
|
...effect.aura,
|
|
utilityTags: [...(effect.aura.utilityTags ?? []), ROM_BREATH_ERASE_THREAT_TAG],
|
|
},
|
|
} : effect);
|
|
}
|
|
|
|
if (row.sourceSkillId === 498745) return [stateAura("industry-fire", ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire, null)];
|
|
if (row.sourceSkillId === 498748) return [stateAura("lightning-display", ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay, null)];
|
|
if (row.sourceSkillId === 498751) return [stateAura("elements-of-pride", ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride, null)];
|
|
if (row.sourceSkillId === 499813) return [stateAura("soul-speed", ROM_MAGE_WARLOCK_PASSIVE_TAGS.soulSpeed, null)];
|
|
if (row.sourceSkillId === 499814) return [stateAura("elemental-extraction", ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction, null)];
|
|
if (row.sourceSkillId === 498757) return [
|
|
...translated,
|
|
stateAura("soul-stepping-cast", ROM_SOUL_STEPPING_CAST_TAG, 6_000),
|
|
];
|
|
return null;
|
|
}
|
|
|
|
function elementsOfPrideAura(school: "fire" | "nature", value: number): AbilityEffect {
|
|
return {
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: {
|
|
id: `rom-mage-warlock-elements-${school}`,
|
|
name: `Elements of Pride: ${school}`,
|
|
disposition: "buff",
|
|
durationMs: 13_000,
|
|
maxStacks: 2,
|
|
stackBehavior: "add",
|
|
modifiers: [{ kind: "damage", direction: "dealt", school, operation: "percent", value, perStack: true }],
|
|
},
|
|
sourceEffectType: -1,
|
|
sourceAuraType: -1,
|
|
miscValue: 0,
|
|
triggerSpellId: 498751,
|
|
};
|
|
}
|
|
|
|
/** Add pair-elite riders to an executable cast after conditional source selection. */
|
|
export function romMageWarlockCastEffects(
|
|
ability: AbilityDefinition,
|
|
effects: readonly AbilityEffect[],
|
|
passiveTags: ReadonlySet<string>,
|
|
eruptionProcRoll = 1,
|
|
): readonly AbilityEffect[] {
|
|
if (ability.source !== "runewaker") return effects;
|
|
|
|
if ([490204, 491150].includes(ability.dbcSpellId)
|
|
&& passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.eruption)
|
|
&& eruptionProcRoll < ROM_COA_MAGE_ERUPTION_BASE_PROC_CHANCE) {
|
|
const doubledDamage = effects.map((effect) => scaledDamage(effect, 2));
|
|
return passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlame)
|
|
&& !passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlameCooldown)
|
|
? [...doubledDamage, ...controlFlameTriggeredAuras()]
|
|
: doubledDamage;
|
|
}
|
|
|
|
if (ability.dbcSpellId === 497964 && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire)) {
|
|
return [
|
|
{
|
|
kind: "damage",
|
|
school: "fire",
|
|
coefficient: 0,
|
|
basePoints: INDUSTRY_FIRE_HIT_BASE,
|
|
pointsPerLevel: INDUSTRY_FIRE_HIT_PER_LEVEL,
|
|
sourceLevel: 1,
|
|
},
|
|
{
|
|
kind: "dot",
|
|
school: "fire",
|
|
coefficient: 0,
|
|
basePoints: INDUSTRY_FIRE_DOT_BASE,
|
|
pointsPerLevel: INDUSTRY_FIRE_DOT_PER_LEVEL,
|
|
sourceLevel: 1,
|
|
ticks: 24,
|
|
intervalMs: 1_000,
|
|
tag: "rom-industry-fire",
|
|
},
|
|
];
|
|
}
|
|
|
|
let result = effects;
|
|
if (passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay)) {
|
|
result = result.map((effect) => {
|
|
if (effect.kind === "apply-aura" && [491169, 498749].includes(ability.dbcSpellId)) {
|
|
if (effect.aura.utilityTags?.includes("rom-charged")) {
|
|
return { ...effect, aura: { ...effect.aura, durationMs: 18_000 } };
|
|
}
|
|
if (effect.aura.modifiers?.some((modifier) => modifier.kind === "stat"
|
|
&& modifier.stat === "magical-critical-strike")) {
|
|
return {
|
|
...effect,
|
|
aura: {
|
|
...effect.aura,
|
|
durationMs: 18_000,
|
|
modifiers: [
|
|
...effect.aura.modifiers,
|
|
{ kind: "damage" as const, direction: "dealt" as const, school: "nature" as const,
|
|
operation: "percent" as const, value: 0.01 },
|
|
],
|
|
},
|
|
};
|
|
}
|
|
}
|
|
return effect;
|
|
});
|
|
}
|
|
|
|
if (passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride)) {
|
|
if ([491150, 498755, 499812].includes(ability.dbcSpellId)) {
|
|
result = [...result, elementsOfPrideAura("nature", 0.02)];
|
|
}
|
|
if ([491170, 498756, 499851].includes(ability.dbcSpellId)) {
|
|
result = [...result, elementsOfPrideAura("fire", 0.01)];
|
|
}
|
|
}
|
|
|
|
if (ability.dbcSpellId === 491170 && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction)) {
|
|
// Hidden level-70 Electric Explosion 499851 replaces the normal branch's
|
|
// 620232 hit with 623668; its Charged 623669 hit keeps the same values.
|
|
const damage = result.filter((effect) => effect.kind === "damage");
|
|
const normal = damage.at(-1);
|
|
if (damage.length >= 2 && normal) result = result.map((effect) => effect === normal ? {
|
|
kind: "damage" as const,
|
|
school: "nature" as const,
|
|
coefficient: 0,
|
|
basePoints: 65,
|
|
pointsPerLevel: 15,
|
|
sourceLevel: 1,
|
|
} : effect);
|
|
}
|
|
|
|
if (ability.dbcSpellId === 498757 && !passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.soulSpeed)) {
|
|
result = result.filter((effect) => effect.kind !== "apply-aura"
|
|
|| (effect.sourceAuraType !== 24 && effect.sourceAuraType !== 51));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Apply only after Electric Explosion or source 499812 critically hits. */
|
|
export function romMageWarlockCriticalEffects(
|
|
ability: AbilityDefinition,
|
|
passiveTags: ReadonlySet<string>,
|
|
): readonly AbilityEffect[] {
|
|
if (!passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction)
|
|
|| ![491170, 498755, 498756, 499812, 499851].includes(ability.dbcSpellId)) return [];
|
|
return [{
|
|
kind: "apply-aura",
|
|
recipient: "ability-target",
|
|
aura: {
|
|
id: "rom-mage-warlock-elemental-extraction-debuff",
|
|
name: "Elemental Extraction",
|
|
disposition: "debuff",
|
|
durationMs: 10_000,
|
|
maxStacks: 1,
|
|
stackBehavior: "refresh",
|
|
modifiers: [
|
|
{ kind: "stat", stat: "armor", operation: "percent", value: -0.3 },
|
|
{ kind: "stat", stat: "magical-defense", operation: "percent", value: -0.3 },
|
|
],
|
|
},
|
|
sourceEffectType: -1,
|
|
sourceAuraType: -1,
|
|
miscValue: 0,
|
|
triggerSpellId: 499814,
|
|
}];
|
|
}
|
|
|
|
/** Lua script 02557: reset Burst on a surviving target once per six seconds. */
|
|
export function romFireLightningBurstReset(
|
|
ability: AbilityDefinition,
|
|
targetAlive: boolean,
|
|
guardActive: boolean,
|
|
): { readonly reset: boolean; readonly guardDurationMs: number } {
|
|
const reset = ability.dbcSpellId === 498759 && targetAlive && !guardActive;
|
|
return { reset, guardDurationMs: reset ? ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS : 0 };
|
|
}
|
|
|
|
/** Breath Erase source 621506: ten percent less generated threat for five seconds. */
|
|
export function romMageWarlockThreatScale(activeTags: ReadonlySet<string>): number {
|
|
return activeTags.has(ROM_BREATH_ERASE_THREAT_TAG) ? 0.9 : 1;
|
|
}
|