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 []; }