forked from phenom/healer-man
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
|
|
|
|
export const WOW335_DEATH_KNIGHT_WANDERING_PLAGUE_TALENT_ID = "talent-2003";
|
|
export const WOW335_DEATH_KNIGHT_WANDERING_PLAGUE_DAMAGE_ABILITY_ID = "wow335-death-knight-wandering-plague";
|
|
export const WOW335_DEATH_KNIGHT_WANDERING_PLAGUE_DISEASE_TAGS = [
|
|
"blood-plague",
|
|
"frost-fever",
|
|
] as const;
|
|
|
|
const WANDERING_PLAGUE_TARGET_RADIUS_YARDS = 8;
|
|
const WANDERING_PLAGUE_INTERNAL_COOLDOWN_MS = 1_000;
|
|
const WANDERING_PLAGUE_TALENT = WOW335_TALENT_NODES.find(
|
|
(node) => node.id === WOW335_DEATH_KNIGHT_WANDERING_PLAGUE_TALENT_ID,
|
|
);
|
|
const DEATH_KNIGHT_GLOBAL_MELEE_CRIT_TALENT_SPECS = [
|
|
{
|
|
talentId: "talent-1943",
|
|
rankDescriptionPattern: /chance to critically hit with weapons, spells and abilities by (\d+(?:\.\d+)?)%/i,
|
|
},
|
|
{
|
|
talentId: "talent-2043",
|
|
rankDescriptionPattern: /critical strike chance with weapons and spells by (\d+(?:\.\d+)?)% at all times/i,
|
|
},
|
|
] as const;
|
|
const WANDERING_PLAGUE_DISEASE_TAG_SET = new Set<string>(WOW335_DEATH_KNIGHT_WANDERING_PLAGUE_DISEASE_TAGS);
|
|
|
|
export interface Wow335DeathKnightWanderingPlagueProfile {
|
|
readonly rank: number;
|
|
readonly extraDamagePercent: number;
|
|
readonly targetRadiusYards: number;
|
|
readonly internalCooldownMs: number;
|
|
}
|
|
|
|
export interface Wow335DeathKnightWanderingPlagueProc {
|
|
readonly extraDamage: number;
|
|
readonly targetRadiusYards: number;
|
|
readonly nextReadyAt: number;
|
|
}
|
|
|
|
/** Read the splash amount from the local generated 3.3.5a talent rank. */
|
|
export function wow335DeathKnightWanderingPlagueProfileForRank(
|
|
rank: number,
|
|
): Wow335DeathKnightWanderingPlagueProfile | null {
|
|
const normalizedRank = Math.max(0, Math.min(3, Math.trunc(rank)));
|
|
if (normalizedRank <= 0) return null;
|
|
const effect = WANDERING_PLAGUE_TALENT?.rankEffects[normalizedRank - 1]?.effects.find(
|
|
(candidate) => candidate.auraType === 4,
|
|
);
|
|
const extraDamagePercent = Math.max(0, Math.trunc(effect?.basePoints ?? 0));
|
|
if (extraDamagePercent <= 0) return null;
|
|
return {
|
|
rank: normalizedRank,
|
|
extraDamagePercent,
|
|
targetRadiusYards: WANDERING_PLAGUE_TARGET_RADIUS_YARDS,
|
|
internalCooldownMs: WANDERING_PLAGUE_INTERNAL_COOLDOWN_MS,
|
|
};
|
|
}
|
|
|
|
export function wow335DeathKnightWanderingPlagueIsDiseaseTag(tag: string | undefined): boolean {
|
|
return tag !== undefined && WANDERING_PLAGUE_DISEASE_TAG_SET.has(tag);
|
|
}
|
|
|
|
/** Read the two generated global melee-crit passives for the live proc chance. */
|
|
export function wow335DeathKnightWanderingPlagueMeleeCritBonusPercentForTalentRanks(
|
|
talentRanks: Readonly<Record<string, number>>,
|
|
): number {
|
|
return DEATH_KNIGHT_GLOBAL_MELEE_CRIT_TALENT_SPECS.reduce((total, spec) => {
|
|
const node = WOW335_TALENT_NODES.find((candidate) => candidate.id === spec.talentId);
|
|
const rank = Math.max(0, Math.min(node?.rankDescriptions.length ?? 0,
|
|
Math.trunc(talentRanks[spec.talentId] ?? 0)));
|
|
if (rank <= 0) return total;
|
|
const description = node?.rankDescriptions[rank - 1] ?? "";
|
|
const match = description.match(spec.rankDescriptionPattern);
|
|
return total + (match ? Number(match[1]) : 0);
|
|
}, 0);
|
|
}
|
|
|
|
/** Resolve a qualifying disease tick against the source's live melee critical chance. */
|
|
export function wow335DeathKnightWanderingPlagueProcForDiseaseTick(
|
|
rank: number,
|
|
tag: string | undefined,
|
|
tickDamage: number,
|
|
meleeCriticalChancePercent: number,
|
|
roll: number,
|
|
now: number,
|
|
readyAt: number,
|
|
): Wow335DeathKnightWanderingPlagueProc | null {
|
|
const profile = wow335DeathKnightWanderingPlagueProfileForRank(rank);
|
|
if (!profile
|
|
|| !wow335DeathKnightWanderingPlagueIsDiseaseTag(tag)
|
|
|| !Number.isFinite(tickDamage)
|
|
|| tickDamage <= 0
|
|
|| !Number.isFinite(meleeCriticalChancePercent)
|
|
|| meleeCriticalChancePercent <= 0
|
|
|| !Number.isFinite(roll)
|
|
|| roll < 0
|
|
|| roll >= Math.min(100, meleeCriticalChancePercent) / 100
|
|
|| !Number.isFinite(now)
|
|
|| !Number.isFinite(readyAt)
|
|
|| now < readyAt) return null;
|
|
|
|
return {
|
|
// Damage is integer-valued in this runtime; round the source percentage once.
|
|
extraDamage: Math.max(0, Math.round(tickDamage * profile.extraDamagePercent / 100)),
|
|
targetRadiusYards: profile.targetRadiusYards,
|
|
nextReadyAt: now + profile.internalCooldownMs,
|
|
};
|
|
}
|