import type { AbilityDefinition, AbilityEffect, AbilityRankDefinition } from "./abilityCatalog"; import type { ActiveAura } from "./combatAuras"; import { runtimeTalentNodeById } from "./talentRuntimeCatalog"; const THE_BEAST_WITHIN_NODE_ID = "talent-1803"; export const WOW335_HUNTER_LOCK_AND_LOAD_TALENT_ID = "talent-1306"; export const WOW335_HUNTER_ENTRAPMENT_TALENT_ID = "talent-1304"; export const WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG = "wow335-hunter-lock-and-load-ready"; export const WOW335_HUNTER_LOCK_AND_LOAD_ICD_TAG = "wow335-hunter-lock-and-load-icd"; export interface BeastWithinRankMechanics { readonly description: string; readonly procChance: number; readonly additionalDamage: number; readonly manaCostReduction: number; readonly durationMs: number; } export interface LockAndLoadRankMechanics { readonly description: string; readonly trapProcChance: number; readonly periodicProcChance: number; readonly freeCastCount: number; readonly durationMs: number; readonly internalCooldownMs: number; } export interface EntrapmentRankMechanics { readonly description: string; readonly frostTrapSlowBonus: number; readonly rootProcChance: number; readonly rootDurationMs: number; } function tooltipNumber(description: string, pattern: RegExp): number | null { const match = description.match(pattern); if (!match) return null; const value = Number(match[1]); return Number.isFinite(value) ? value : null; } /** Read The Beast Within's mechanics from the generated talent rank text. */ export function beastWithinRankMechanics(rank: number): BeastWithinRankMechanics | null { const node = runtimeTalentNodeById(THE_BEAST_WITHIN_NODE_ID); const description = node?.rankDescriptions[Math.trunc(rank) - 1]; if (!description) return null; const procChance = tooltipNumber(description, /you have a (\d+(?:\.\d+)?)% chance/i); const additionalDamage = tooltipNumber(description, /causing (\d+(?:\.\d+)?)% additional damage/i); const manaCostReduction = tooltipNumber(description, /reducing mana costs of all spells by (\d+(?:\.\d+)?)%/i); const durationSeconds = tooltipNumber(description, /for (\d+(?:\.\d+)?) seconds/i); if (procChance === null || additionalDamage === null || manaCostReduction === null || durationSeconds === null) { return null; } return { description, procChance: Math.max(0, Math.min(1, procChance / 100)), additionalDamage: additionalDamage / 100, manaCostReduction: manaCostReduction / 100, durationMs: Math.max(0, Math.round(durationSeconds * 1_000)), }; } /** Read Lock and Load's rank values from its generated tooltip text. */ export function lockAndLoadRankMechanics(rank: number): LockAndLoadRankMechanics | null { const node = runtimeTalentNodeById(WOW335_HUNTER_LOCK_AND_LOAD_TALENT_ID); const description = node?.rankDescriptions[Math.trunc(rank) - 1]; if (!description) return null; const trapProcPercent = tooltipNumber(description, /trapping a target[^.]*?has a (\d+(?:\.\d+)?)% chance/i); const periodicProcPercent = tooltipNumber(description, /periodic damage[^.]*?has a (\d+(?:\.\d+)?)% to trigger/i); const freeCastCount = tooltipNumber(description, /your next (\d+) Arcane Shot or Explosive Shot/i); const cooldownSeconds = tooltipNumber(description, /this effect has a (\d+(?:\.\d+)?) sec cooldown/i); if (trapProcPercent === null || periodicProcPercent === null || freeCastCount === null || cooldownSeconds === null) { return null; } const durationMs = Math.max(0, Math.round(cooldownSeconds * 1_000)); return { description, trapProcChance: Math.max(0, Math.min(1, trapProcPercent / 100)), periodicProcChance: Math.max(0, Math.min(1, periodicProcPercent / 100)), freeCastCount: Math.max(0, Math.trunc(freeCastCount)), durationMs, internalCooldownMs: durationMs, }; } /** Read the authored Entrapment slow/root values from the generated talent tooltip. */ export function entrapmentRankMechanics(rank: number): EntrapmentRankMechanics | null { const normalizedRank = Math.trunc(rank); const node = runtimeTalentNodeById(WOW335_HUNTER_ENTRAPMENT_TALENT_ID); const description = node?.rankDescriptions[normalizedRank - 1]; if (!description) return null; const slowPercent = tooltipNumber(description, /increases the slow effect of Frost Trap by (\d+(?:\.\d+)?)%/i); const rootDurationSeconds = tooltipNumber(description, /preventing them from moving for (\d+(?:\.\d+)?) seconds/i); if (slowPercent === null || rootDurationSeconds === null) return null; // The local Hunter DBC Entrapment rank rows carry 2/4/6 as their proc base // points; their companion child aura supplies the root duration. const rootProcChance = [0, 0.02, 0.04, 0.06][normalizedRank]; if (rootProcChance === undefined) return null; return { description, frostTrapSlowBonus: slowPercent / 100, rootProcChance, rootDurationMs: Math.max(0, Math.round(rootDurationSeconds * 1_000)), }; } /** Spell.dbc child-aura durations for Freezing Trap's three learned ranks. */ export function freezingTrapDurationMs(spellId: number): number { switch (spellId) { case 1499: return 10_000; case 14310: return 15_000; case 14311: case 60192: case 60202: return 20_000; default: return 20_000; } } export function consumeLockAndLoadCharge( auras: readonly ActiveAura[], auraInstanceId: string, ): readonly ActiveAura[] { return auras.flatMap((aura) => aura.instanceId !== auraInstanceId ? [aura] : aura.stacks > 1 ? [{ ...aura, stacks: aura.stacks - 1 }] : []); } function applyBeastWithinRankMechanics( effects: readonly AbilityEffect[], mechanics: BeastWithinRankMechanics, ): readonly AbilityEffect[] { return effects.map((effect) => { if (effect.kind !== "apply-aura") return effect; return { ...effect, aura: { ...effect.aura, description: mechanics.description, durationMs: mechanics.durationMs, modifiers: effect.aura.modifiers?.map((modifier) => { if (modifier.kind === "resource" && modifier.property === "cost" && modifier.resource === "power") { return { ...modifier, value: -mechanics.manaCostReduction }; } if (modifier.kind === "damage" && modifier.direction === "dealt") { return { ...modifier, value: mechanics.additionalDamage }; } return modifier; }), }, }; }); } /** Replace the duplicate DBC child-rank text and payload with the generated talent's actual ranks. */ export function applyGeneratedBeastWithinTalent(ability: AbilityDefinition): AbilityDefinition { if (ability.classId !== "hunter" || ability.name !== "The Beast Within") return ability; const node = runtimeTalentNodeById(THE_BEAST_WITHIN_NODE_ID); const sourceRank = ability.ranks?.[0]; if (!node || !sourceRank || node.rankDescriptions.length === 0) return ability; const ranks: AbilityRankDefinition[] = []; for (const [index, description] of node.rankDescriptions.entries()) { const mechanics = beastWithinRankMechanics(index + 1); if (!mechanics) return ability; ranks.push({ ...sourceRank, rank: index + 1, description, effects: applyBeastWithinRankMechanics(sourceRank.effects, mechanics), }); } const firstRank = ranks[0]; if (!firstRank) return ability; return { ...ability, talentNodeId: node.id, description: firstRank.description, effects: firstRank.effects, ranks, }; }