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

189 lines
7.4 KiB
TypeScript

import type { AbilityDefinition, AbilityEffect, AbilityRankDefinition } from "./abilityCatalog";
import type { ActiveAura } from "./combatAuras";
import { runtimeTalentNodeById } from "./talentRuntimeCatalog";
const THE_BEAST_WITHIN_NODE_ID = "talent-1803";
export const WOW335_HUNTER_LOCK_AND_LOAD_TALENT_ID = "talent-1306";
export const WOW335_HUNTER_ENTRAPMENT_TALENT_ID = "talent-1304";
export const WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG = "wow335-hunter-lock-and-load-ready";
export const WOW335_HUNTER_LOCK_AND_LOAD_ICD_TAG = "wow335-hunter-lock-and-load-icd";
export interface BeastWithinRankMechanics {
readonly description: string;
readonly procChance: number;
readonly additionalDamage: number;
readonly manaCostReduction: number;
readonly durationMs: number;
}
export interface LockAndLoadRankMechanics {
readonly description: string;
readonly trapProcChance: number;
readonly periodicProcChance: number;
readonly freeCastCount: number;
readonly durationMs: number;
readonly internalCooldownMs: number;
}
export interface EntrapmentRankMechanics {
readonly description: string;
readonly frostTrapSlowBonus: number;
readonly rootProcChance: number;
readonly rootDurationMs: number;
}
function tooltipNumber(description: string, pattern: RegExp): number | null {
const match = description.match(pattern);
if (!match) return null;
const value = Number(match[1]);
return Number.isFinite(value) ? value : null;
}
/** Read The Beast Within's mechanics from the generated talent rank text. */
export function beastWithinRankMechanics(rank: number): BeastWithinRankMechanics | null {
const node = runtimeTalentNodeById(THE_BEAST_WITHIN_NODE_ID);
const description = node?.rankDescriptions[Math.trunc(rank) - 1];
if (!description) return null;
const procChance = tooltipNumber(description, /you have a (\d+(?:\.\d+)?)% chance/i);
const additionalDamage = tooltipNumber(description, /causing (\d+(?:\.\d+)?)% additional damage/i);
const manaCostReduction = tooltipNumber(description, /reducing mana costs of all spells by (\d+(?:\.\d+)?)%/i);
const durationSeconds = tooltipNumber(description, /for (\d+(?:\.\d+)?) seconds/i);
if (procChance === null || additionalDamage === null || manaCostReduction === null || durationSeconds === null) {
return null;
}
return {
description,
procChance: Math.max(0, Math.min(1, procChance / 100)),
additionalDamage: additionalDamage / 100,
manaCostReduction: manaCostReduction / 100,
durationMs: Math.max(0, Math.round(durationSeconds * 1_000)),
};
}
/** Read Lock and Load's rank values from its generated tooltip text. */
export function lockAndLoadRankMechanics(rank: number): LockAndLoadRankMechanics | null {
const node = runtimeTalentNodeById(WOW335_HUNTER_LOCK_AND_LOAD_TALENT_ID);
const description = node?.rankDescriptions[Math.trunc(rank) - 1];
if (!description) return null;
const trapProcPercent = tooltipNumber(description, /trapping a target[^.]*?has a (\d+(?:\.\d+)?)% chance/i);
const periodicProcPercent = tooltipNumber(description, /periodic damage[^.]*?has a (\d+(?:\.\d+)?)% to trigger/i);
const freeCastCount = tooltipNumber(description, /your next (\d+) Arcane Shot or Explosive Shot/i);
const cooldownSeconds = tooltipNumber(description, /this effect has a (\d+(?:\.\d+)?) sec cooldown/i);
if (trapProcPercent === null || periodicProcPercent === null || freeCastCount === null || cooldownSeconds === null) {
return null;
}
const durationMs = Math.max(0, Math.round(cooldownSeconds * 1_000));
return {
description,
trapProcChance: Math.max(0, Math.min(1, trapProcPercent / 100)),
periodicProcChance: Math.max(0, Math.min(1, periodicProcPercent / 100)),
freeCastCount: Math.max(0, Math.trunc(freeCastCount)),
durationMs,
internalCooldownMs: durationMs,
};
}
/** Read the authored Entrapment slow/root values from the generated talent tooltip. */
export function entrapmentRankMechanics(rank: number): EntrapmentRankMechanics | null {
const normalizedRank = Math.trunc(rank);
const node = runtimeTalentNodeById(WOW335_HUNTER_ENTRAPMENT_TALENT_ID);
const description = node?.rankDescriptions[normalizedRank - 1];
if (!description) return null;
const slowPercent = tooltipNumber(description, /increases the slow effect of Frost Trap by (\d+(?:\.\d+)?)%/i);
const rootDurationSeconds = tooltipNumber(description, /preventing them from moving for (\d+(?:\.\d+)?) seconds/i);
if (slowPercent === null || rootDurationSeconds === null) return null;
// The local Hunter DBC Entrapment rank rows carry 2/4/6 as their proc base
// points; their companion child aura supplies the root duration.
const rootProcChance = [0, 0.02, 0.04, 0.06][normalizedRank];
if (rootProcChance === undefined) return null;
return {
description,
frostTrapSlowBonus: slowPercent / 100,
rootProcChance,
rootDurationMs: Math.max(0, Math.round(rootDurationSeconds * 1_000)),
};
}
/** Spell.dbc child-aura durations for Freezing Trap's three learned ranks. */
export function freezingTrapDurationMs(spellId: number): number {
switch (spellId) {
case 1499: return 10_000;
case 14310: return 15_000;
case 14311:
case 60192:
case 60202: return 20_000;
default: return 20_000;
}
}
export function consumeLockAndLoadCharge(
auras: readonly ActiveAura[],
auraInstanceId: string,
): readonly ActiveAura[] {
return auras.flatMap((aura) => aura.instanceId !== auraInstanceId
? [aura]
: aura.stacks > 1 ? [{ ...aura, stacks: aura.stacks - 1 }] : []);
}
function applyBeastWithinRankMechanics(
effects: readonly AbilityEffect[],
mechanics: BeastWithinRankMechanics,
): readonly AbilityEffect[] {
return effects.map((effect) => {
if (effect.kind !== "apply-aura") return effect;
return {
...effect,
aura: {
...effect.aura,
description: mechanics.description,
durationMs: mechanics.durationMs,
modifiers: effect.aura.modifiers?.map((modifier) => {
if (modifier.kind === "resource" && modifier.property === "cost" && modifier.resource === "power") {
return { ...modifier, value: -mechanics.manaCostReduction };
}
if (modifier.kind === "damage" && modifier.direction === "dealt") {
return { ...modifier, value: mechanics.additionalDamage };
}
return modifier;
}),
},
};
});
}
/** Replace the duplicate DBC child-rank text and payload with the generated talent's actual ranks. */
export function applyGeneratedBeastWithinTalent(ability: AbilityDefinition): AbilityDefinition {
if (ability.classId !== "hunter" || ability.name !== "The Beast Within") return ability;
const node = runtimeTalentNodeById(THE_BEAST_WITHIN_NODE_ID);
const sourceRank = ability.ranks?.[0];
if (!node || !sourceRank || node.rankDescriptions.length === 0) return ability;
const ranks: AbilityRankDefinition[] = [];
for (const [index, description] of node.rankDescriptions.entries()) {
const mechanics = beastWithinRankMechanics(index + 1);
if (!mechanics) return ability;
ranks.push({
...sourceRank,
rank: index + 1,
description,
effects: applyBeastWithinRankMechanics(sourceRank.effects, mechanics),
});
}
const firstRank = ranks[0];
if (!firstRank) return ability;
return {
...ability,
talentNodeId: node.id,
description: firstRank.description,
effects: firstRank.effects,
ranks,
};
}