import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; import type { AuraDefinition, AuraModifier, AuraProcDefinition } from "./combatAuras"; import type { RomGeneratedAbilityLike } from "./romAbilitySemantics"; import { ROM_ROGUE_COERCE_TAG, ROM_ROGUE_DAGGER_MASTERY_TAG, ROM_ROGUE_OFF_HAND_MASTERY_TAG, ROM_ROGUE_TREASURE_HUNTER_TAG, romRogueCoerceLowBlowDamageMultiplier, } from "./romRogueMechanics"; import { ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG as ROM_CHAOS_DAGGER_TAG } from "./romRogueChaosDagger"; import { ROM_ROGUE_CHAMPION_NERVE_GEAR_MARKER_TAG, ROM_ROGUE_CHAMPION_NERVE_GEAR_PASSIVE_SPELL_ID, } from "./romRogueChampionNerveGearRuntime"; export const ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG = ROM_CHAOS_DAGGER_TAG; /** @deprecated Use ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG for current CoA behavior. */ export const ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG = ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG; export const ROM_ROGUE_PRIEST_SINISTER_METHODS_TAG = "rom-rogue-priest-sinister-methods"; export const ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY = "rom-rogue-priest-sinister-methods"; export const ROM_SCOUT_PRIEST_SNIPER_TAG = "rom-scout-priest-sniper"; export const ROM_SCOUT_MAGE_WIND_RETURN_TAG = "rom-scout-mage-wind-return"; export const ROM_SCOUT_PRIEST_REVIVAL_ENGRAVING_TAG = "rom-scout-priest-revival-engraving"; export const ROM_SCOUT_WARDEN_FOREST_GUIDANCE_TAG = "rom-scout-warden-forest-guidance"; const ROM_SCOUT_KNIGHT_ENHANCED_ARMOR_PERFECTION_TAG = "rom-scout-knight-enhanced-armor-perfection"; // Local RuneWaker component 501823 sets Enhanced Armor's duration to 600s. const ROM_SCOUT_KNIGHT_ENHANCED_ARMOR_DURATION_MS = 600_000; /** RuneWaker itemobject/shop rows supply Poison Powder to the Scout/Rogue loadout. */ export const ROM_SCOUT_ROGUE_POISON_POWDER_SUPPLY = Object.freeze({ itemId: 200563, name: "Poison Powder", quantity: "unlimited", } as const); function firstDamage(effects: readonly AbilityEffect[]): AbilityEffect[] { return effects.filter((effect) => effect.kind === "damage").slice(0, 1); } function scaleDamageEffects(effects: readonly AbilityEffect[], factor: number): readonly AbilityEffect[] { return effects.map((effect) => effect.kind === "damage" || effect.kind === "dot" ? { ...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.kind !== "damage" || effect.bonusFlat === undefined ? {} : { bonusFlat: effect.bonusFlat * factor }), } : effect); } function taggedPassive( sourceId: number, name: string, tag: string, proc?: AuraProcDefinition, ): AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: `rom-pair-passive-${sourceId}`, name, disposition: "buff", durationMs: null, utilityTags: [tag], ...(proc ? { procs: [proc] } : {}), }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: sourceId, }]; } function taggedRoguePassive(sourceId: number, name: string, tag: string): AbilityEffect[] { return [{ kind: "apply-aura", recipient: "caster", aura: { id: `rom-aura:${sourceId}:rogue-passive`, name, disposition: "buff", durationMs: null, utilityTags: [tag], }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: sourceId, }]; } function customProc( sourceId: number, trigger: "hit" | "crit", actionId: string, options: { readonly chance?: number; readonly internalCooldownMs?: number; readonly data?: Readonly>; } = {}, ): AuraProcDefinition { return { id: `rom-pair-proc-${sourceId}`, triggers: trigger === "hit" ? ["hit", "crit"] : trigger, chance: options.chance ?? 1, ...(options.internalCooldownMs ? { internalCooldownMs: options.internalCooldownMs } : {}), action: { kind: "custom", id: actionId, ...(options.data ? { data: options.data } : {}) }, }; } function timedCasterAura( sourceId: number, name: string, durationMs: number, modifiers: readonly AuraModifier[], ): AbilityEffect { return { kind: "apply-aura", recipient: "caster", aura: { id: `rom-pair-buff-${sourceId}`, name, disposition: "buff", durationMs, modifiers, }, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: sourceId, }; } /** * Source-specific translations for the accepted Scout paired-class * mechanics. Passive tags and custom proc IDs are consumed by combat-store * transitions; they intentionally do not apply unrelated generic stat buffs. */ export function overrideRomScoutRogueEffects( row: RomGeneratedAbilityLike, translated: readonly AbilityEffect[], ): readonly AbilityEffect[] | null { switch (row.sourceSkillId) { case 497206: { // Dodge arms the usable spell; its child applies the three-second concealment. if (row.classId !== "rom-rogue" || row.secondaryClassId !== "rom-mage") return null; const concealment = row.source?.components?.find((component) => component.id === 502597); const durationMs = Math.max(1_000, Math.round((concealment?.effectTime ?? 3) * 1_000)); return [{ kind: "apply-aura", recipient: "caster", aura: { id: "rom-rogue:instant-shadow", name: "Instant Shadow", disposition: "buff", durationMs, maxStacks: 1, stackBehavior: "refresh", utilityTags: ["stealth"], }, sourceEffectType: row.source?.sourceEffectType ?? -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 502597, }]; } case 490504: return taggedRoguePassive(490504, "Dagger Mastery", ROM_ROGUE_DAGGER_MASTERY_TAG); case 493019: if (row.classId !== "rom-rogue" || row.secondaryClassId !== "rom-scout") return null; // Current CoA Coerce directly increases Low Blow damage and reduces its // Energy cost. Its older RuneWaker below-30% temporary buff is obsolete. return taggedRoguePassive(493019, "Coerce", ROM_ROGUE_COERCE_TAG); case 490316: return taggedRoguePassive(490316, "Treasure Hunter", ROM_ROGUE_TREASURE_HUNTER_TAG); case 490143: // Keep the source modifiers scoped to actual off-hand attacks. A generic // melee aura would incorrectly buff the main hand as well. return taggedRoguePassive(490143, "Off Hand Mastery", ROM_ROGUE_OFF_HAND_MASTERY_TAG); case 493020: { // Successful Low Blow/Blind Spot hits trigger the local 10s crit buff. if (row.classId !== "rom-rogue" || row.secondaryClassId !== "rom-priest") return null; const buff = row.source?.components?.find((component) => component.id === 503265); const criticalModifier = buff?.abilityModifiers?.find((modifier) => modifier.type === 18); const cooldown = row.source?.components?.find((component) => component.id === 503300); const durationMs = Math.max(0, Math.round((buff?.effectTime ?? 0) * 1_000)); const cooldownMs = Math.max(0, Math.round((cooldown?.effectTime ?? 0) * 1_000)); const proc = criticalModifier && durationMs > 0 && cooldownMs > 0 ? customProc(493020, "hit", "rom-sinister-methods", { data: { criticalStrike: criticalModifier.value, durationMs, cooldownMs }, }) : undefined; return taggedPassive(493020, "Sinister Methods", ROM_ROGUE_PRIEST_SINISTER_METHODS_TAG, proc); } case ROM_ROGUE_CHAMPION_NERVE_GEAR_PASSIVE_SPELL_ID: if (row.classId !== "rom-rogue" || row.secondaryClassId !== "rom-champion") return null; // The timed child only triggers after an elite skill use. Keep a learned // marker, not its generic aura, so party passive loading cannot grant the // attack-speed/dodge payload on an unrelated cast. return taggedPassive( ROM_ROGUE_CHAMPION_NERVE_GEAR_PASSIVE_SPELL_ID, "Nerve Gear", ROM_ROGUE_CHAMPION_NERVE_GEAR_MARKER_TAG, ); case 491178: if (row.classId !== "rom-rogue" || row.secondaryClassId !== "rom-mage") return null; // Current CoA reuses this exact Rogue/Mage ID as passive Chaos Dagger. // Keep a marker for learned-passive consumers such as Day of Rain; do // not register the incompatible RuneWaker Throw child or local aura. return taggedRoguePassive(491178, "Chaos Dagger", ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG); case 491552: if (row.classId !== "rom-rogue") return null; return translated.map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), "rom-491552"] }, } : effect); case 490445: // Bow/crossbow accuracy must not grant melee hit; keep its range scope explicit. return translated.filter((effect) => effect.kind !== "apply-aura" || effect.sourceAuraType !== 54) .map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, modifiers: effect.aura.modifiers?.map((modifier) => modifier.kind === "stat" && modifier.stat === "physical-hit" ? { ...modifier, stat: "ranged-hit" } : modifier), }, } : effect); case 490505: // Bow and crossbow each carry an equivalent +11% attack-speed modifier in the source. return translated.filter((effect) => effect.kind !== "apply-aura" || effect.sourceAuraType !== 70); case 490400: // Piercing Arrow: one projectile, then a line hit behind its target. return firstDamage(translated); case 490424: { // Combo Shot: three impacts of the same source shot. const hit = firstDamage(translated)[0]; return hit ? [hit, hit, hit] : translated; } case 490457: { // Reflected Shot: two jumps, each retaining half the prior hit. const hit = firstDamage(translated)[0]; return hit?.kind === "damage" ? [{ ...hit, falloffPerJump: 0.5 }] : translated; } case 498803: // Soul Stab's first hit is unconditional; its second hit requires Weakened. return translated.filter((effect) => effect.kind === "damage"); case 490354: // Vanish is an aggro/stealth transition, not a self-heal. return translated.filter((effect) => effect.kind !== "heal").map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, utilityTags: ["stealth", "rom-vanish"] } } : effect); case 491493: { // Weak Spot's buff is component 501895, not its permanent hit modifier. const source = row.source?.components?.find((component) => component.id === 501895); const critical = source?.abilityModifiers?.find((modifier) => modifier.type === 18)?.value ?? 20; const durationMs = Math.max(1_000, Math.round((source?.effectTime ?? 15) * 1_000)); return [ ...firstDamage(translated), timedCasterAura(491493, "Weak Spot critical strike", durationMs, [ { kind: "stat", stat: "physical-critical-strike", operation: "flat", value: critical }, ]), ]; } case 492342: return taggedPassive(492342, "Lasso Master", "rom-lasso-master"); case 492916: return taggedPassive(492916, "Exploiting Shot", "rom-exploiting-shot", customProc(492916, "crit", "rom-exploiting-shot", { internalCooldownMs: 20_000 })); case 493006: return taggedPassive(493006, "Accurate Targeting", "rom-accurate-targeting", customProc(493006, "hit", "rom-accurate-targeting", { internalCooldownMs: 30_000 })); case 491489: if (row.classId !== "rom-scout" || row.secondaryClassId !== "rom-warrior") return null; return taggedPassive(491489, "Enhanced Throat Attack", "rom-enhanced-throat-attack", customProc(491489, "hit", "rom-enhanced-throat-attack")); case 491532: if (row.classId !== "rom-rogue" || row.secondaryClassId !== "rom-scout") return null; return taggedPassive(491532, "Slit Throat", "rom-slit-throat", customProc(491532, "hit", "rom-slit-throat")); case 492344: if (row.classId !== "rom-scout" || row.secondaryClassId !== "rom-priest") return null; return taggedPassive(492344, "Sniper", ROM_SCOUT_PRIEST_SNIPER_TAG); case 492346: if (row.classId !== "rom-scout" || row.secondaryClassId !== "rom-knight") return null; return taggedPassive(492346, "Enhanced Armor Perfection", ROM_SCOUT_KNIGHT_ENHANCED_ARMOR_PERFECTION_TAG); case 493007: return taggedPassive(493007, "Deadly Counterattack", "rom-deadly-counterattack", customProc(493007, "hit", "rom-deadly-counterattack", { internalCooldownMs: 20_000 })); case 493253: return taggedPassive(493253, "Poisonous Spit", "rom-poisonous-spit", customProc(493253, "hit", "rom-poisonous-spit", { internalCooldownMs: 20_000 })); case 499548: if (row.classId !== "rom-scout" || row.secondaryClassId !== "rom-mage") return null; // Component 623070/623083 describes the six-second buff and HoT earned // after skill 499549 expires. That referenced skill has no local player // row, so do not grant its child payload when the passive is learned. return taggedPassive(499548, "Wind Return", ROM_SCOUT_MAGE_WIND_RETURN_TAG); case 499552: if (row.classId !== "rom-scout" || row.secondaryClassId !== "rom-priest") return null; // Component 623087 is a pet-stat aura triggered after skill 490443 hits. // The trigger has no local player row and pet stats are not modeled, so // keep only the learned marker instead of applying it to the Scout. return taggedPassive(499552, "Revival Engraving", ROM_SCOUT_PRIEST_REVIVAL_ENGRAVING_TAG); case 499575: if (row.classId !== "rom-scout" || row.secondaryClassId !== "rom-warden") return null; // Component 623117 belongs on the target hit by skill 499576. That // player-facing skill has no local generated row, so keep only the // learned marker instead of applying its timed vulnerability to the // passive owner at combat initialization. return taggedPassive(499575, "Forest Guidance", ROM_SCOUT_WARDEN_FOREST_GUIDANCE_TAG); case 499534: return taggedPassive(499534, "Master Reflected Shot", "rom-master-reflected-shot"); case 499536: return taggedPassive(499536, "Meridian Attack", "rom-meridian-attack"); case 491526: return taggedPassive(491526, "Throwing Mastery", "rom-throwing-mastery"); case 491527: // The bonus applies to one-hand axes only, never all melee damage. return taggedPassive(491527, "One-Hand Axe Mastery", "rom-one-hand-axe-mastery"); case 492349: return taggedPassive(492349, "Whirlwind Mastery", "rom-whirlwind-mastery"); case 491339: // A negative RuneWaker value means reduced received healing. return translated.map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, modifiers: effect.aura.modifiers?.map((modifier) => modifier.kind === "healing" && modifier.direction === "taken" ? { ...modifier, value: -Math.abs(modifier.value) } : modifier), }, } : effect); case 492625: // Source attack component 620287 must not make its victim immune to physical damage. return translated.filter((effect) => effect.kind === "damage" || effect.kind === "dot"); case 492920: { // Poison Spray spreads Infection's ten-second poison to nearby enemies. const hit = firstDamage(translated)[0]; const poison: AbilityEffect = { kind: "dot", coefficient: 0, basePoints: 12, pointsPerLevel: 4.2, sourceLevel: 1, ticks: 5, intervalMs: 2_000, tag: "rom-poisonous-infection", }; return hit ? [hit, poison] : [poison]; } case 493017: // Decay's empty indefinite state is not its timed critical buff. return translated.filter((effect) => effect.kind !== "apply-aura" || effect.aura.modifiers?.length || effect.aura.procs?.length || effect.aura.utilityTags?.length); case 493257: return taggedPassive(493257, "Reclaim Energy", "rom-reclaim-energy", customProc(493257, "hit", "rom-reclaim-energy", { chance: 0.25 })); case 499476: return taggedPassive(499476, "Toxic Splash", "rom-toxic-splash", // 493296 replaces Infection and can generate 20 rage on a successful hit. customProc(499476, "hit", "rom-infection-rage", { chance: 0.25 })); case 499480: return taggedPassive(499480, "Poison Battle Technique", "rom-poison-battle-technique"); default: return null; } } /** Select source-conditional pair effects after the target's existing states are known. */ export function romScoutRogueCastEffects( ability: AbilityDefinition, effects: readonly AbilityEffect[], passiveTags: readonly string[], targetTags: readonly string[], ): readonly AbilityEffect[] { const passives = new Set(passiveTags); const target = new Set(targetTags); const coerceDamageMultiplier = romRogueCoerceLowBlowDamageMultiplier({ classId: ability.classId, abilitySource: ability.source, abilitySpellId: ability.dbcSpellId, passiveTags, }); if (coerceDamageMultiplier !== 1) return scaleDamageEffects(effects, coerceDamageMultiplier); if (ability.dbcSpellId === 490152 && passives.has(ROM_SCOUT_KNIGHT_ENHANCED_ARMOR_PERFECTION_TAG)) { // Component 501823 is the source's rank payload: Enhanced Armor lasts 600s. return effects.map((effect) => effect.kind === "apply-aura" ? { ...effect, aura: { ...effect.aura, durationMs: ROM_SCOUT_KNIGHT_ENHANCED_ARMOR_DURATION_MS } } : effect); } if (ability.dbcSpellId === 490457) { // Replacement 499535 / component 623051 raises ranged DPS from 0.75 to 0.85. const masteryFactor = passives.has("rom-master-reflected-shot") ? 0.85 / 0.75 : 1; return effects.map((effect) => effect.kind === "damage" ? { ...effect, coefficient: effect.coefficient * masteryFactor, ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * masteryFactor }), ...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints * masteryFactor }), ...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel * masteryFactor }), ...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient * masteryFactor }), ...(effect.bonusFlat === undefined ? {} : { bonusFlat: effect.bonusFlat * masteryFactor }), falloffPerJump: passives.has("rom-poisonous-spit") ? 1 : effect.falloffPerJump, } : effect); } if (ability.dbcSpellId === 498803) { let seenDamage = 0; const weakened = target.has("rom-weakened"); return effects.filter((effect) => { if (effect.kind !== "damage") return true; seenDamage += 1; return seenDamage === 1 || weakened; }); } if (ability.dbcSpellId === 490451 && passives.has("rom-meridian-attack")) { // 499537/623061 replaces Wrist Attack: -30 physical and magical damage for 10s. return effects.map((effect) => effect.kind === "apply-aura" && effect.recipient !== "caster" ? { ...effect, aura: { ...effect.aura, durationMs: 10_000, modifiers: [ { kind: "damage" as const, direction: "dealt" as const, school: "physical" as const, operation: "percent" as const, value: -0.30 }, { kind: "damage" as const, direction: "dealt" as const, attackKind: "spell" as const, operation: "percent" as const, value: -0.30 }, ], }, } : effect); } if (ability.dbcSpellId === 492920 && passives.has("rom-toxic-splash")) { // 499477 replaces Poison Spray. 493296 is its additional infection hit; // 622992 is the six-second poison it spreads to nearby targets. return [ ...effects.filter((effect) => effect.kind !== "dot"), // 620287: 0.85 main-hand DPS + 0.20 Dexterity, then +7.5% skill-level DPS. { kind: "damage", coefficient: 1.11375, coefficientPerLevel: 0.06375, sourceLevel: 1 }, { kind: "dot", coefficient: 0, basePoints: 16.2, pointsPerLevel: 4.2, sourceLevel: 1, ticks: 3, intervalMs: 2_000, tag: "rom-622992" }, ]; } if (ability.dbcSpellId === 490133 && passives.has("rom-poison-battle-technique")) { // 499481/620286 raises Whirlwind's main-hand DPS coefficient 0.70 -> 0.75. return effects.map((effect) => effect.kind === "damage" ? { ...effect, coefficient: effect.coefficient * 0.75 / 0.70, ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * 0.75 / 0.70 }), } : effect); } if (ability.dbcSpellId === 490454 && passives.has("rom-lasso-master")) { return [...effects, { kind: "status", status: "disarm", durationMs: 10_000, maxTargets: 1, breakOnDamage: true, }]; } if (ability.dbcSpellId === 490309) { return [ ...effects, ...(passives.has("rom-throwing-mastery") ? [{ kind: "status" as const, status: "slow" as const, durationMs: 5_000, magnitude: 0.4 }] : []), { kind: "cooldown-reset", mode: "reduce", abilityName: "Day of Rain", amountMs: 1_000 }, ]; } if (ability.dbcSpellId === 492621) { const hasSourcePoison = target.has("rom-deadly-poison") || target.has("rom-501690"); let seenHits = 0; return effects.filter((effect) => { if (effect.kind !== "damage") return true; seenHits += 1; return seenHits === 1 || (seenHits === 2 && hasSourcePoison); }); } return effects; } /** Day of Rain has an exact 50% miss chance while the Rogue/Mage Chaos Dagger passive is learned. */ export function romRogueMageDayOfRainBaseMissPercent( classId: string, secondaryClassId: string | null | undefined, abilitySpellId: number | undefined, activeAuraTags: readonly string[], attackerHitPercent: number, ): number | null { if (classId !== "rom-rogue" || secondaryClassId !== "rom-mage" || abilitySpellId !== 491183 || !activeAuraTags.includes(ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG)) return null; const hitPercent = Number.isFinite(attackerHitPercent) ? Math.max(0, Math.min(100, attackerHitPercent)) : 0; // combatAttackTable subtracts attacker hit rating from base miss chance. // Offset it here so the tooltip's 50% rate remains exact at any hit rating. return 50 + hitPercent; } /** 499481/622993 adds a hit only to targets already carrying 622992's poison. */ export function romScoutRogueWhirlwindBonusEffect( ability: AbilityDefinition, passiveTags: readonly string[], targetTags: readonly string[], ): Extract | null { if (ability.dbcSpellId !== 490133 || !passiveTags.includes("rom-poison-battle-technique") || !targetTags.includes("rom-622992")) return null; return { kind: "damage", coefficient: 0.50, coefficientPerLevel: 0.03, sourceLevel: 1 }; } export type RomScoutRogueProcActionSpec = | { readonly kind: "aura"; readonly recipient: "caster" | "target"; readonly aura: AuraDefinition; readonly provenance: "source" | "hybrid-estimate"; readonly cooldownMs?: number; } | { readonly kind: "dot"; readonly recipient: "target"; readonly effect: Extract; readonly provenance: "source" | "hybrid-estimate"; } | { readonly kind: "resource"; readonly recipient: "caster"; readonly resource: "energy" | "rage"; readonly amount: number; readonly provenance: "source" | "hybrid-estimate"; } | { readonly kind: "damage"; readonly recipient: "target"; readonly effect: Extract; readonly provenance: "source" | "hybrid-estimate"; } | { readonly kind: "status"; readonly recipient: "target"; readonly status: "silence"; readonly durationMs: number; readonly provenance: "source"; }; /** * A pure result for combat-store proc execution. `triggeringSourceSkillId` is * the RuneWaker skill ID of the attack that generated the hit/crit event. * The level-60/70 replacement skills 499477 and 499481 are represented by * their learned passive tags and base-ID cast upgrades, not custom procs. */ export function romScoutRogueProcActionSpec( actionId: string, triggeringSourceSkillId: number, _casterPoisonous: boolean, data: Readonly> = {}, ): RomScoutRogueProcActionSpec | null { switch (actionId) { case "rom-enhanced-throat-attack": case "rom-slit-throat": if (triggeringSourceSkillId !== 490428) return null; return { kind: "status", recipient: "target", status: "silence", durationMs: 4_000, provenance: "source" }; case "rom-sinister-methods": { if (![490323, 490311].includes(triggeringSourceSkillId)) return null; const criticalStrike = data.criticalStrike; const durationMs = data.durationMs; const cooldownMs = data.cooldownMs; if (typeof criticalStrike !== "number" || !Number.isFinite(criticalStrike) || typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs <= 0 || typeof cooldownMs !== "number" || !Number.isFinite(cooldownMs) || cooldownMs <= 0) return null; return { kind: "aura", recipient: "caster", aura: { id: "rom-pair-sinister-methods-critical", name: "Sinister Methods", disposition: "buff", durationMs, maxStacks: 1, stackBehavior: "refresh", modifiers: [{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: criticalStrike }], }, cooldownMs, provenance: "source", }; } case "rom-accurate-targeting": { if (triggeringSourceSkillId !== 490423) return null; return { kind: "aura", recipient: "caster", aura: { id: "rom-scout-accurate-targeting", name: "Accurate Targeting", disposition: "buff", durationMs: 15_000, // Source component 503255. modifiers: [{ kind: "stat", stat: "physical-attack", operation: "flat", value: 50 }], }, provenance: "source", }; } case "rom-exploiting-shot": if (triggeringSourceSkillId !== 490423) return null; return { kind: "aura", recipient: "target", aura: { id: "rom-pair-exploiting-shot-vulnerability", name: "Exploiting Shot", disposition: "debuff", durationMs: 12_000, // Source component 503169. maxStacks: 5, // Explicit hybrid cap; source tooltip references a cap without a numeric value. stackBehavior: "add", modifiers: [{ kind: "damage", direction: "taken", school: "physical", operation: "percent", value: 0.02 }], }, provenance: "hybrid-estimate", }; case "rom-deadly-counterattack": if (triggeringSourceSkillId !== 490400) return null; return { kind: "aura", recipient: "caster", aura: { id: "rom-pair-deadly-counterattack-crit", name: "Deadly Counterattack", disposition: "buff", durationMs: 5_000, // Hybrid estimate; source specifies a 20s internal cooldown, not buff duration. modifiers: [{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: 20 }], }, provenance: "hybrid-estimate", }; case "rom-poisonous-spit": // Poison Powder (item 200563) is supplied as an unlimited bag reagent by // the Scout/Rogue loadout model. Do not use Rogue's separate Poison // Bottle aura (490329) as a proxy for carrying this item. if (ROM_SCOUT_ROGUE_POISON_POWDER_SUPPLY.quantity !== "unlimited" || ![490457, 490424].includes(triggeringSourceSkillId)) return null; return { kind: "dot", recipient: "target", effect: { kind: "dot", coefficient: 0, basePoints: 7.1, pointsPerLevel: 2.1, sourceLevel: 1, ticks: 4, intervalMs: 2_000, tag: "rom-poisonous-spit", }, provenance: "source", // 503522; Poison Powder is a carried item, not Rogue's Poison Bottle buff. }; case "rom-reclaim-energy": if (triggeringSourceSkillId === 490306) return { kind: "resource", recipient: "caster", resource: "energy", amount: 15, provenance: "hybrid-estimate", }; if (triggeringSourceSkillId === 492625) return { kind: "resource", recipient: "caster", resource: "rage", amount: 10, provenance: "hybrid-estimate", }; return null; case "rom-infection-rage": return triggeringSourceSkillId === 492625 ? { kind: "resource", recipient: "caster", resource: "rage", amount: 20, provenance: "hybrid-estimate" } : null; case "rom-toxic-splash": // 499477 is applied as an upgrade of base 492920. case "rom-poison-battle-technique": // 499481 is applied as an upgrade of base 490133. return null; default: return null; } }