Files
healer-man/src/game/wow335DeathKnightSpellDeflectionRuntime.ts
2026-10-02 10:38:12 -04:00

116 lines
4.5 KiB
TypeScript

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<Record<EnemyDamageSchool, number>> = {
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));
}