Files
healer-man/src/game/wow335DeathKnightWanderingPlagueRuntime.ts
T

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,
};
}