import { WOW335_TALENT_NODES } from "./wow335Talents.generated"; import type { EnemyDamageSchool } from "./enemyAttacks"; export const WOW335_DEATH_KNIGHT_SPELL_DEFLECTION_TALENT_ID = "talent-2018"; const WOW335_DAMAGE_SCHOOL_MASKS: Readonly> = { physical: 1, holy: 2, fire: 4, nature: 8, frost: 16, shadow: 32, arcane: 64, }; /** Translate the incoming combat-school label into its 3.3.5 DBC school bit. */ export function wow335DeathKnightSpellDeflectionSchoolMask(school: EnemyDamageSchool): number { return WOW335_DAMAGE_SCHOOL_MASKS[school]; } const SPELL_DEFLECTION_NODE = WOW335_TALENT_NODES.find( (node) => node.id === WOW335_DEATH_KNIGHT_SPELL_DEFLECTION_TALENT_ID, ); export interface Wow335DeathKnightSpellDeflectionProfile { readonly rank: number; readonly absorbFraction: number; readonly procChanceSource: "live-parry"; readonly auraType: 69; readonly schoolMask: number; } /** Read rank reduction and spell-school mask from generated local talent rows. */ export function wow335DeathKnightSpellDeflectionProfile( rank: number, ): Wow335DeathKnightSpellDeflectionProfile | null { const normalizedRank = Math.trunc(rank); const rankEffect = SPELL_DEFLECTION_NODE?.rankEffects[normalizedRank - 1]; const description = SPELL_DEFLECTION_NODE?.rankDescriptions[normalizedRank - 1]; if (normalizedRank < 1 || !rankEffect || !description || rankEffect.procCharges !== 0 || rankEffect.recoveryTimeMs !== 0 || rankEffect.categoryRecoveryTimeMs !== 0) return null; const values = description.match( /^You have a chance equal to your Parry chance of taking (\d+(?:\.\d+)?)% less damage from a direct damage spell\.$/, ); const effect = rankEffect.effects.find((candidate) => candidate.effectType === 6 && candidate.auraType === 69); const reductionPercent = Number(values?.[1]); if (!values || !effect || effect.miscValue !== 126 || !Number.isFinite(reductionPercent) || reductionPercent <= 0 || reductionPercent >= 100) return null; return { rank: normalizedRank, absorbFraction: reductionPercent / 100, procChanceSource: "live-parry", auraType: effect.auraType, schoolMask: effect.miscValue, }; } export interface Wow335DeathKnightSpellDeflectionEvent { /** True only for one direct spell-damage hit; periodic ticks and weapon hits are excluded. */ readonly directDamageSpell: boolean; /** DBC school mask for the incoming spell hit; Spell Deflection has mask 126. */ readonly schoolMask: number; /** Defender's current, fully derived Parry chance in percentage points, such as 7.5. */ readonly parryChancePercent: number; /** Deterministic unit roll in [0, 1). */ readonly roll: number; /** Resolved spell damage before this talent's absorb; misses and zero-damage hits cannot proc. */ readonly damage: number; } /** * Proc chance equals the defender's current Parry chance. The generated aura is * School Absorb (69), limited by school mask 126, and applies to direct hits. */ export function wow335DeathKnightSpellDeflectionShouldProc( rank: number, event: Wow335DeathKnightSpellDeflectionEvent, ): boolean { const profile = wow335DeathKnightSpellDeflectionProfile(rank); if (!profile || !event.directDamageSpell || !Number.isSafeInteger(event.schoolMask) || event.schoolMask <= 0 || (event.schoolMask & profile.schoolMask) === 0 || !Number.isFinite(event.damage) || event.damage <= 0 || !Number.isFinite(event.parryChancePercent) || !Number.isFinite(event.roll) || event.roll < 0 || event.roll >= 1) return false; const chance = Math.min(100, Math.max(0, event.parryChancePercent)) / 100; return chance > 0 && event.roll < chance; } /** * Return the amount absorbed from this hit. Generated rows specify the percent, * not fractional-damage rounding; this simulator floors to whole damage points. */ export function wow335DeathKnightSpellDeflectionAbsorbedAmount( damage: number, rank: number, ): number { const profile = wow335DeathKnightSpellDeflectionProfile(rank); if (!profile || !Number.isFinite(damage) || damage <= 0) return 0; return Math.min(Math.floor(damage), Math.floor(Math.floor(damage) * profile.absorbFraction)); } /** Apply the generated partial absorb after the proc roll has succeeded. */ export function wow335DeathKnightSpellDeflectionDamage( damage: number, rank: number, ): number { if (!Number.isFinite(damage) || damage <= 0) return Math.max(0, Number.isFinite(damage) ? Math.floor(damage) : 0); return Math.max(0, Math.floor(damage) - wow335DeathKnightSpellDeflectionAbsorbedAmount(damage, rank)); }