forked from phenom/healer-man
116 lines
4.5 KiB
TypeScript
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));
|
|
}
|