import { WOW335_TALENT_NODES } from "./wow335Talents.generated"; import generatedData from "./wow335AbilityData.generated.json"; export const WOW335_WARRIOR_ANGER_MANAGEMENT_TALENT_ID = "talent-137"; export const WOW335_WARRIOR_ANGER_MANAGEMENT_CHARGE_AURA_TAG = "wow335-warrior-anger-management-charge"; const SOURCE = WOW335_TALENT_NODES.find((node) => ( node.id === WOW335_WARRIOR_ANGER_MANAGEMENT_TALENT_ID )); const CHARGE_SOURCE = generatedData.classes.warrior.find((ability) => ability.name === "Charge"); export function wow335WarriorChargeRank(spellId: number): number { return CHARGE_SOURCE?.ranks.find((rank) => rank.spellId === spellId)?.rank ?? 0; } export interface Wow335WarriorAngerManagementProfile { readonly rank: number; readonly ragePerTick: number; readonly tickIntervalMs: number; readonly chargeBonusRage: number; readonly chargeDamageReductionPercent: number; readonly chargeDamageReductionDurationMs: number; readonly chargeDamageReductionSpellId: number | null; } export interface Wow335WarriorChargeRageProfile { readonly chargeRank: number; readonly chargeSpellId: number; readonly angerManagementRank: number; readonly baseRage: number; readonly angerManagementBonusRage: number; readonly totalRage: number; } /** * Resolves the base Charge Rage and the selected Anger Management rank. * Generated Charge tooltips hide the amount as "listed amount" while the * local dummy effect contains 90/120/150. WotLKDB's Charge rank-1 tooltip * confirms that dummy value 90 means 9 Rage; the same local field conversion * is applied to ranks 2 and 3 (120/150 => 12/15 Rage). The database supplies * only the missing unit scale; local rank rows remain the data source. */ export function wow335WarriorChargeRageProfile( chargeRank: number, angerManagementRank: number, ): Wow335WarriorChargeRageProfile | null { const maxChargeRank = CHARGE_SOURCE?.ranks.length ?? 0; const safeChargeRank = Math.min(maxChargeRank, Math.max(0, Math.trunc( Number.isFinite(chargeRank) ? chargeRank : 0, ))); const charge = CHARGE_SOURCE?.ranks.find((rank) => rank.rank === safeChargeRank); const rageEffect = charge?.effects.find((effect) => effect.effectType === 3 && effect.implicitTargetA === 1); if (safeChargeRank === 0 || !charge || !rageEffect || rageEffect.basePoints <= 0 || rageEffect.basePoints % 10 !== 0) { return null; } const maxTalentRank = SOURCE?.rankSpellIds.length ?? 0; const safeTalentRank = Math.min(maxTalentRank, Math.max(0, Math.trunc( Number.isFinite(angerManagementRank) ? angerManagementRank : 0, ))); const talentProfile = wow335WarriorAngerManagementProfile(safeTalentRank); const baseRage = rageEffect.basePoints / 10; const angerManagementBonusRage = talentProfile?.chargeBonusRage ?? 0; return { chargeRank: safeChargeRank, chargeSpellId: charge.spellId, angerManagementRank: safeTalentRank, baseRage, angerManagementBonusRage, totalRage: baseRage + angerManagementBonusRage, }; } /** * Reads the local generated rank tooltip and rank effects. The three rank * tooltips specify passive Rage and Charge bonuses; the maximum-rank tooltip * additionally describes Charge's four-second damage reduction. Rank 1 and * 2 mention that capstone in their text only as a rank-3 unlock, so it is not * exposed until the final rank is allocated. */ export function wow335WarriorAngerManagementProfile( rank: number, ): Wow335WarriorAngerManagementProfile | null { const rankCount = SOURCE?.rankSpellIds.length ?? 0; const safeRank = Math.min(rankCount, Math.max(0, Math.trunc(Number.isFinite(rank) ? rank : 0))); const description = SOURCE?.rankDescriptions[safeRank - 1]; const rankEffect = SOURCE?.rankEffects[safeRank - 1]; if (safeRank === 0 || !description || !rankEffect) return null; const rage = description.match(/generates (\d+) rage per (\d+) seconds/i); const chargeBonus = description.match(/Charge ability by (\d+)/i); if (!rage || !chargeBonus) return null; const ragePerTick = Number(rage[1]); const tickIntervalMs = Number(rage[2]) * 1_000; const chargeBonusRage = Number(chargeBonus[1]); if (![ragePerTick, tickIntervalMs, chargeBonusRage].every(Number.isFinite) || ragePerTick < 0 || tickIntervalMs <= 0 || chargeBonusRage < 0) return null; let chargeDamageReductionPercent = 0; let chargeDamageReductionDurationMs = 0; let chargeDamageReductionSpellId: number | null = null; if (safeRank === rankCount) { const reduction = description.match(/Charge also reduces your damage taken by (\d+)% for (\d+) seconds/i); if (reduction) { chargeDamageReductionPercent = Number(reduction[1]); chargeDamageReductionDurationMs = Number(reduction[2]) * 1_000; chargeDamageReductionSpellId = rankEffect.effects.find((effect) => effect.triggerSpellId > 0)?.triggerSpellId ?? null; } } return { rank: safeRank, ragePerTick, tickIntervalMs, chargeBonusRage, chargeDamageReductionPercent, chargeDamageReductionDurationMs, chargeDamageReductionSpellId, }; }