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

154 lines
5.8 KiB
TypeScript

import type { AuraDefinition } from "./combatAuras";
import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
export const WOW335_MAGE_WINTERS_CHILL_TALENT_ID = "talent-68";
export const WOW335_MAGE_WINTERS_CHILL_TAG = "wow335-mage-winters-chill";
const WINTERS_CHILL_TALENT = WOW335_TALENT_NODES.find((talent) => (
talent.id === WOW335_MAGE_WINTERS_CHILL_TALENT_ID
));
function tooltipNumber(description: string, pattern: RegExp): number {
const parsed = Number(description.match(pattern)?.[1]);
return Number.isFinite(parsed) ? parsed : 0;
}
const WINTERS_CHILL_EFFECT_DESCRIPTION = WINTERS_CHILL_TALENT?.rankDescriptions[0] ?? "";
const WINTERS_CHILL_TARGET_CRITICAL_CHANCE_PER_STACK_PERCENT = tooltipNumber(
WINTERS_CHILL_EFFECT_DESCRIPTION,
/increasing the chance spells will critically strike the target by\s+(\d+(?:\.\d+)?)%/i,
);
const WINTERS_CHILL_TARGET_HEALING_REDUCTION_PER_STACK_PERCENT = tooltipNumber(
WINTERS_CHILL_EFFECT_DESCRIPTION,
/reducing healing received by\s+(\d+(?:\.\d+)?)%/i,
);
const WINTERS_CHILL_DURATION_MS = tooltipNumber(
WINTERS_CHILL_EFFECT_DESCRIPTION,
/for\s+(\d+(?:\.\d+)?)\s+seconds/i,
) * 1_000;
const WINTERS_CHILL_MAXIMUM_STACKS = tooltipNumber(
WINTERS_CHILL_EFFECT_DESCRIPTION,
/stacks up to\s+(\d+)\s+times/i,
);
function normalizedRank(rank: number): number {
return Number.isFinite(rank)
? Math.max(0, Math.min(WINTERS_CHILL_TALENT?.rankEffects.length ?? 0, Math.trunc(rank)))
: 0;
}
function generatedRankValue(rank: number, pattern: RegExp): number {
const description = WINTERS_CHILL_TALENT?.rankDescriptions[normalizedRank(rank) - 1] ?? "";
return tooltipNumber(description, pattern);
}
export const WOW335_MAGE_WINTERS_CHILL_AURA: AuraDefinition = {
id: "wow335:mage:winters-chill",
name: "Winter's Chill",
description: `Each stack increases incoming spell critical chance by ${WINTERS_CHILL_TARGET_CRITICAL_CHANCE_PER_STACK_PERCENT}% and reduces healing received by ${WINTERS_CHILL_TARGET_HEALING_REDUCTION_PER_STACK_PERCENT}% for ${WINTERS_CHILL_DURATION_MS / 1_000} seconds.`,
disposition: "debuff",
durationMs: WINTERS_CHILL_DURATION_MS,
maxStacks: WINTERS_CHILL_MAXIMUM_STACKS,
stackBehavior: "add",
uniqueBySource: false,
// Local child data is absent; WotLKDB 3.3.5a spell 12579 identifies it as Magic-dispellable.
dispelCategory: "magic",
modifiers: [{
kind: "healing",
direction: "taken",
operation: "percent",
value: -WINTERS_CHILL_TARGET_HEALING_REDUCTION_PER_STACK_PERCENT / 100,
}],
utilityTags: [WOW335_MAGE_WINTERS_CHILL_TAG],
};
export interface Wow335MageWintersChillEvent {
readonly abilityName: string;
readonly attackKind: string;
readonly school: string;
readonly periodic: boolean;
readonly landedDamage: number;
}
export interface Wow335MageWintersChillProfile {
readonly casterCriticalChanceBonusPercent: number;
readonly procChancePercent: number;
readonly targetCriticalChanceBonusPercentPerStack: number;
readonly targetHealingReceivedReductionPercentPerStack: number;
readonly durationMs: number;
readonly maximumStacks: number;
}
/**
* Local generated Mage talent node 68, ranks 11180/28592/28593. Its rank
* descriptions give the passive crit bonus, proc chance, and custom 8-second
* child effect values; the stock 3.3.5a child row is not this custom payload.
*/
export function wow335MageWintersChillProfileForRank(rank: number): Wow335MageWintersChillProfile {
const normalized = normalizedRank(rank);
const rankEffect = WINTERS_CHILL_TALENT?.rankEffects[normalized - 1];
return {
casterCriticalChanceBonusPercent: generatedRankValue(
normalized,
/critical strike chance with all spells and abilities by\s+(\d+(?:\.\d+)?)%/i,
),
procChancePercent: Math.max(0, Math.min(100, rankEffect?.procChance ?? 0)),
targetCriticalChanceBonusPercentPerStack: WINTERS_CHILL_TARGET_CRITICAL_CHANCE_PER_STACK_PERCENT,
targetHealingReceivedReductionPercentPerStack: WINTERS_CHILL_TARGET_HEALING_REDUCTION_PER_STACK_PERCENT,
durationMs: WINTERS_CHILL_DURATION_MS,
maximumStacks: WINTERS_CHILL_MAXIMUM_STACKS,
};
}
export function wow335MageWintersChillProcChancePercent(
rank: number,
event: Wow335MageWintersChillEvent,
): number {
const profile = wow335MageWintersChillProfileForRank(rank);
const eligible = profile.procChancePercent > 0
&& event.attackKind === "spell"
&& event.school === "frost"
&& !event.periodic
&& Number.isFinite(event.landedDamage)
&& event.landedDamage > 0
&& event.abilityName.toLowerCase() !== "frozen orb";
return eligible ? profile.procChancePercent : 0;
}
export function wow335MageWintersChillShouldProc(
rank: number,
event: Wow335MageWintersChillEvent,
roll: number,
): boolean {
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) return false;
return roll < wow335MageWintersChillProcChancePercent(rank, event) / 100;
}
export function wow335MageWintersChillStacksAfterEvent(
currentStacks: number,
rank: number,
event: Wow335MageWintersChillEvent,
roll: number,
): number {
const profile = wow335MageWintersChillProfileForRank(rank);
const stacks = Number.isFinite(currentStacks)
? Math.max(0, Math.min(profile.maximumStacks, Math.trunc(currentStacks)))
: 0;
return wow335MageWintersChillShouldProc(rank, event, roll)
? Math.min(profile.maximumStacks, stacks + 1)
: stacks;
}
export function wow335MageWintersChillTargetModifiers(stacks: number): {
readonly criticalChanceBonusPercent: number;
readonly healingReceivedReductionPercent: number;
} {
const normalized = Number.isFinite(stacks)
? Math.max(0, Math.min(WINTERS_CHILL_MAXIMUM_STACKS, Math.trunc(stacks)))
: 0;
return {
criticalChanceBonusPercent: normalized * WINTERS_CHILL_TARGET_CRITICAL_CHANCE_PER_STACK_PERCENT,
healingReceivedReductionPercent: normalized * WINTERS_CHILL_TARGET_HEALING_REDUCTION_PER_STACK_PERCENT,
};
}