122 lines
5.4 KiB
TypeScript
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 },
|
|
],
|
|
};
|
|
}
|