Files
healer-man/src/game/wow335PaladinTalentRuntime.ts
T
2026-09-30 09:57:13 -04:00

122 lines
5.4 KiB
TypeScript

import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
export interface WoW335PaladinRedoubtProfile {
readonly blockChancePercent: number;
readonly procChance: number;
readonly strengthPercent: number;
readonly damageTakenReductionPercent: number;
readonly durationMs: number;
}
export interface WoW335PaladinVindicationProfile {
readonly rank: number;
readonly procChance: number;
readonly attackPowerReduction: number;
readonly durationMs: number;
}
export interface WoW335PaladinBlessedLifeProfile {
readonly rank: number;
readonly procChance: number;
readonly incomingDamageMultiplier: number;
}
export const WOW335_PALADIN_BLESSED_LIFE_TALENT_ID = "talent-1744";
const WOW335_PALADIN_VINDICATION_TALENT_ID = "talent-1633";
// The local child aura row has no duration; WotLKDB spell 67 supplies 10 sec.
// https://wotlkdb.com/?spell=67
const WOW335_PALADIN_VINDICATION_DURATION_MS = 10_000;
/** Resolve only the generated rank chance and half-damage payload for Blessed Life. */
export function wow335PaladinBlessedLifeProfile(talentRank: number): WoW335PaladinBlessedLifeProfile | null {
const rank = Math.min(3, Math.max(0, Math.trunc(Number.isFinite(talentRank) ? talentRank : 0)));
if (rank === 0) return null;
const talent = WOW335_TALENT_NODES.find((node) => node.id === WOW335_PALADIN_BLESSED_LIFE_TALENT_ID);
const rankEffect = talent?.rankEffects[rank - 1];
const rawDescription = talent?.rawRankDescriptions[rank - 1] ?? "";
const descriptionChance = rawDescription.match(/All attacks against you have a \$h% chance to cause half damage\./i);
const displayedChance = talent?.rankDescriptions[rank - 1]?.match(
/All attacks against you have a (\d+(?:\.\d+)?)% chance to cause half damage\./i,
);
if (!rankEffect || !descriptionChance || !displayedChance
|| Number(displayedChance[1]) !== rankEffect.procChance) return null;
return {
rank,
procChance: Math.min(1, Math.max(0, rankEffect.procChance / 100)),
incomingDamageMultiplier: 0.5,
};
}
/** Apply the tooltip's half-damage result to an incoming attack roll. */
export function wow335PaladinBlessedLifeDamageMultiplierForRoll(talentRank: number, roll: number): number {
const profile = wow335PaladinBlessedLifeProfile(talentRank);
if (!profile || !Number.isFinite(roll) || roll < 0 || roll >= 1) return 1;
return roll < profile.procChance ? profile.incomingDamageMultiplier : 1;
}
/**
* Resolve the generated Vindication rank chance and raw level-scaled tooltip.
* WotLKDB's child AP value differs from the local tooltip formula, so this
* deliberately uses only local values for proc chance and amount.
*/
export function wow335PaladinVindicationProfile(
talentRank: number,
playerLevel: number,
): WoW335PaladinVindicationProfile | null {
const rank = Math.min(3, Math.max(0, Math.trunc(Number.isFinite(talentRank) ? talentRank : 0)));
if (rank === 0) return null;
const talent = WOW335_TALENT_NODES.find((node) => node.id === WOW335_PALADIN_VINDICATION_TALENT_ID);
const rankEffect = talent?.rankEffects[rank - 1];
const rawDescription = talent?.rawRankDescriptions[rank - 1] ?? "";
if (!rankEffect || !/\$\{\s*4\.5\s*\*\s*\$PL\s*-\s*3\s*\}/.test(rawDescription)) return null;
const level = Math.min(80, Math.max(1, Math.trunc(Number.isFinite(playerLevel) ? playerLevel : 1)));
return {
rank,
procChance: Math.min(1, Math.max(0, rankEffect.procChance / 100)),
attackPowerReduction: Math.max(0, 4.5 * level - 3),
durationMs: WOW335_PALADIN_VINDICATION_DURATION_MS,
};
}
/** Read the rank values from the generated tooltip so local spell rows cannot override them. */
export function wow335PaladinRedoubtProfile(
rankDescription: string | undefined,
): WoW335PaladinRedoubtProfile | null {
if (!rankDescription) return null;
const blockChance = rankDescription.match(/chance to block attacks with a shield by (\d+(?:\.\d+)?)%/i);
const proc = rankDescription.match(
/blocking an attack has a (\d+(?:\.\d+)?)% chance to increase your strength by (\d+(?:\.\d+)?)% and reduce your damage taken by (\d+(?:\.\d+)?)%\. lasts (\d+(?:\.\d+)?) seconds/i,
);
if (!blockChance || !proc) return null;
return {
blockChancePercent: Number(blockChance[1]),
procChance: Number(proc[1]) / 100,
strengthPercent: Number(proc[2]),
damageTakenReductionPercent: Number(proc[3]),
durationMs: Number(proc[4]) * 1_000,
};
}
export function wow335PaladinRedoubtBuff(
profile: WoW335PaladinRedoubtProfile,
strength: number,
) {
const gainedStrength = Math.round(Math.max(0, strength) * profile.strengthPercent / 100);
return {
id: "wow335:paladin:redoubt",
name: "Redoubt",
description: `Increases your Strength by ${profile.strengthPercent}% and reduces your damage taken by ${profile.damageTakenReductionPercent}% for ${profile.durationMs / 1_000} seconds.`,
disposition: "buff" as const,
durationMs: profile.durationMs,
maxStacks: 1,
stackBehavior: "refresh" as const,
utilityTags: ["wow335-paladin-redoubt"],
modifiers: [
{ kind: "stat" as const, stat: "strength", operation: "percent" as const, value: profile.strengthPercent / 100 },
// Paladins gain two attack power for each point of Strength.
{ kind: "stat" as const, stat: "attack-power", operation: "flat" as const, value: gainedStrength * 2 },
{ kind: "damage" as const, direction: "taken" as const, operation: "percent" as const, value: -profile.damageTakenReductionPercent / 100 },
],
};
}