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

122 lines
5.0 KiB
TypeScript

import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
import generatedData from "./wow335AbilityData.generated.json";
export const WOW335_WARRIOR_ANGER_MANAGEMENT_TALENT_ID = "talent-137";
export const WOW335_WARRIOR_ANGER_MANAGEMENT_CHARGE_AURA_TAG = "wow335-warrior-anger-management-charge";
const SOURCE = WOW335_TALENT_NODES.find((node) => (
node.id === WOW335_WARRIOR_ANGER_MANAGEMENT_TALENT_ID
));
const CHARGE_SOURCE = generatedData.classes.warrior.find((ability) => ability.name === "Charge");
export function wow335WarriorChargeRank(spellId: number): number {
return CHARGE_SOURCE?.ranks.find((rank) => rank.spellId === spellId)?.rank ?? 0;
}
export interface Wow335WarriorAngerManagementProfile {
readonly rank: number;
readonly ragePerTick: number;
readonly tickIntervalMs: number;
readonly chargeBonusRage: number;
readonly chargeDamageReductionPercent: number;
readonly chargeDamageReductionDurationMs: number;
readonly chargeDamageReductionSpellId: number | null;
}
export interface Wow335WarriorChargeRageProfile {
readonly chargeRank: number;
readonly chargeSpellId: number;
readonly angerManagementRank: number;
readonly baseRage: number;
readonly angerManagementBonusRage: number;
readonly totalRage: number;
}
/**
* Resolves the base Charge Rage and the selected Anger Management rank.
* Generated Charge tooltips hide the amount as "listed amount" while the
* local dummy effect contains 90/120/150. WotLKDB's Charge rank-1 tooltip
* confirms that dummy value 90 means 9 Rage; the same local field conversion
* is applied to ranks 2 and 3 (120/150 => 12/15 Rage). The database supplies
* only the missing unit scale; local rank rows remain the data source.
*/
export function wow335WarriorChargeRageProfile(
chargeRank: number,
angerManagementRank: number,
): Wow335WarriorChargeRageProfile | null {
const maxChargeRank = CHARGE_SOURCE?.ranks.length ?? 0;
const safeChargeRank = Math.min(maxChargeRank, Math.max(0, Math.trunc(
Number.isFinite(chargeRank) ? chargeRank : 0,
)));
const charge = CHARGE_SOURCE?.ranks.find((rank) => rank.rank === safeChargeRank);
const rageEffect = charge?.effects.find((effect) => effect.effectType === 3 && effect.implicitTargetA === 1);
if (safeChargeRank === 0 || !charge || !rageEffect || rageEffect.basePoints <= 0 || rageEffect.basePoints % 10 !== 0) {
return null;
}
const maxTalentRank = SOURCE?.rankSpellIds.length ?? 0;
const safeTalentRank = Math.min(maxTalentRank, Math.max(0, Math.trunc(
Number.isFinite(angerManagementRank) ? angerManagementRank : 0,
)));
const talentProfile = wow335WarriorAngerManagementProfile(safeTalentRank);
const baseRage = rageEffect.basePoints / 10;
const angerManagementBonusRage = talentProfile?.chargeBonusRage ?? 0;
return {
chargeRank: safeChargeRank,
chargeSpellId: charge.spellId,
angerManagementRank: safeTalentRank,
baseRage,
angerManagementBonusRage,
totalRage: baseRage + angerManagementBonusRage,
};
}
/**
* Reads the local generated rank tooltip and rank effects. The three rank
* tooltips specify passive Rage and Charge bonuses; the maximum-rank tooltip
* additionally describes Charge's four-second damage reduction. Rank 1 and
* 2 mention that capstone in their text only as a rank-3 unlock, so it is not
* exposed until the final rank is allocated.
*/
export function wow335WarriorAngerManagementProfile(
rank: number,
): Wow335WarriorAngerManagementProfile | null {
const rankCount = SOURCE?.rankSpellIds.length ?? 0;
const safeRank = Math.min(rankCount, Math.max(0, Math.trunc(Number.isFinite(rank) ? rank : 0)));
const description = SOURCE?.rankDescriptions[safeRank - 1];
const rankEffect = SOURCE?.rankEffects[safeRank - 1];
if (safeRank === 0 || !description || !rankEffect) return null;
const rage = description.match(/generates (\d+) rage per (\d+) seconds/i);
const chargeBonus = description.match(/Charge ability by (\d+)/i);
if (!rage || !chargeBonus) return null;
const ragePerTick = Number(rage[1]);
const tickIntervalMs = Number(rage[2]) * 1_000;
const chargeBonusRage = Number(chargeBonus[1]);
if (![ragePerTick, tickIntervalMs, chargeBonusRage].every(Number.isFinite)
|| ragePerTick < 0 || tickIntervalMs <= 0 || chargeBonusRage < 0) return null;
let chargeDamageReductionPercent = 0;
let chargeDamageReductionDurationMs = 0;
let chargeDamageReductionSpellId: number | null = null;
if (safeRank === rankCount) {
const reduction = description.match(/Charge also reduces your damage taken by (\d+)% for (\d+) seconds/i);
if (reduction) {
chargeDamageReductionPercent = Number(reduction[1]);
chargeDamageReductionDurationMs = Number(reduction[2]) * 1_000;
chargeDamageReductionSpellId = rankEffect.effects.find((effect) => effect.triggerSpellId > 0)?.triggerSpellId ?? null;
}
}
return {
rank: safeRank,
ragePerTick,
tickIntervalMs,
chargeBonusRage,
chargeDamageReductionPercent,
chargeDamageReductionDurationMs,
chargeDamageReductionSpellId,
};
}