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(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>, ): 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, }; }