119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
import { runtimeTalentNodeById } from "./talentRuntimeCatalog";
|
|
|
|
export const WOW335_HUNTER_GO_FOR_THE_THROAT_TALENT_ID = "talent-1818";
|
|
export const WOW335_HUNTER_GO_FOR_THE_THROAT_INTERNAL_COOLDOWN_MS = 2_000;
|
|
|
|
const HUNTER_GO_FOR_THE_THROAT_DIRECT_ATTACK_KINDS: Readonly<Record<string, "melee" | "ranged">> = Object.freeze({
|
|
"wow335-hunter-aimed-shot": "ranged",
|
|
"wow335-hunter-arcane-shot": "ranged",
|
|
"wow335-hunter-auto-shot": "ranged",
|
|
"wow335-hunter-chimera-shot": "ranged",
|
|
"wow335-hunter-counterattack": "melee",
|
|
"wow335-hunter-kill-shot": "ranged",
|
|
"wow335-hunter-mongoose-bite": "melee",
|
|
"wow335-hunter-multi-shot": "ranged",
|
|
"wow335-hunter-raptor-strike": "melee",
|
|
"wow335-hunter-scatter-shot": "ranged",
|
|
"wow335-hunter-silencing-shot": "ranged",
|
|
"wow335-hunter-steady-shot": "ranged",
|
|
// Simulated Hunter weapon swings use these runtime event IDs rather than
|
|
// the Auto Shot spell row; the player path currently reports its generic
|
|
// basic swing as melee, while party Hunters report their basic swing ranged.
|
|
"party-auto-attack": "ranged",
|
|
"player-auto-attack": "melee",
|
|
"player-auto-attack-offhand": "melee",
|
|
// The core executable catalog keeps these two seeded actions' stable IDs
|
|
// when their local Spell.dbc rank rows are merged.
|
|
"hunter-arcane-shot": "ranged",
|
|
"hunter-steady-shot": "ranged",
|
|
});
|
|
|
|
/**
|
|
* Classifies only local generated Hunter rows whose descriptions/effects name
|
|
* a direct melee strike or ranged shot. Trap, sting, and channel rows are not
|
|
* inferred to be weapon attacks from their damage school or range alone.
|
|
*/
|
|
export function wow335HunterGoForTheThroatAttackKindForAbility(
|
|
abilityId: string,
|
|
): "melee" | "ranged" | null {
|
|
return HUNTER_GO_FOR_THE_THROAT_DIRECT_ATTACK_KINDS[abilityId] ?? null;
|
|
}
|
|
|
|
export interface Wow335HunterGoForTheThroatRankMechanics {
|
|
readonly description: string;
|
|
readonly focusPerProc: number;
|
|
readonly internalCooldownMs: number;
|
|
}
|
|
|
|
export interface Wow335HunterGoForTheThroatProcEvent {
|
|
readonly attackType: "melee" | "ranged" | "other";
|
|
readonly critical: boolean;
|
|
readonly hasLivingActivePet: boolean;
|
|
readonly focusCurrent: number;
|
|
readonly focusMaximum: number;
|
|
readonly lastProcAt: number | null;
|
|
readonly now: number;
|
|
}
|
|
|
|
export interface Wow335HunterGoForTheThroatProcPlan {
|
|
readonly focusGain: number;
|
|
readonly focusAfter: number;
|
|
readonly lastProcAt: number;
|
|
}
|
|
|
|
const talent = runtimeTalentNodeById(WOW335_HUNTER_GO_FOR_THE_THROAT_TALENT_ID);
|
|
|
|
/**
|
|
* Reads the Focus payload from the generated 3.3.5a talent rank text.
|
|
* The generated ranks say 15/30 Focus and share a two-second proc lockout.
|
|
*/
|
|
export function wow335HunterGoForTheThroatRankMechanics(
|
|
rank: number,
|
|
): Wow335HunterGoForTheThroatRankMechanics | null {
|
|
const normalizedRank = Math.trunc(Number.isFinite(rank) ? rank : 0);
|
|
const description = talent?.rankDescriptions[normalizedRank - 1];
|
|
if (!description) return null;
|
|
|
|
const focusMatch = description.match(/tamed beast to generate (\d+(?:\.\d+)?) Focus/i);
|
|
const cooldownMatch = description.match(/cannot occur more than once every (\d+(?:\.\d+)?) seconds/i);
|
|
const focusPerProc = focusMatch ? Number(focusMatch[1]) : Number.NaN;
|
|
const cooldownSeconds = cooldownMatch ? Number(cooldownMatch[1]) : Number.NaN;
|
|
if (!Number.isFinite(focusPerProc) || focusPerProc <= 0
|
|
|| !Number.isFinite(cooldownSeconds) || cooldownSeconds <= 0) return null;
|
|
|
|
return Object.freeze({
|
|
description,
|
|
focusPerProc,
|
|
internalCooldownMs: Math.round(cooldownSeconds * 1_000),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Plans one deterministic pet Focus proc after an eligible Hunter crit.
|
|
* The caller owns pet selection, regeneration, and resource mutation; Focus
|
|
* is capped to the pet's actual current maximum.
|
|
*/
|
|
export function wow335HunterGoForTheThroatProcPlan(
|
|
rank: number,
|
|
event: Wow335HunterGoForTheThroatProcEvent,
|
|
): Wow335HunterGoForTheThroatProcPlan | null {
|
|
const mechanics = wow335HunterGoForTheThroatRankMechanics(rank);
|
|
if (!mechanics
|
|
|| (event.attackType !== "melee" && event.attackType !== "ranged")
|
|
|| !event.critical
|
|
|| !event.hasLivingActivePet
|
|
|| !Number.isFinite(event.now)
|
|
|| (event.lastProcAt !== null
|
|
&& (!Number.isFinite(event.lastProcAt)
|
|
|| event.now - event.lastProcAt < mechanics.internalCooldownMs))) return null;
|
|
|
|
const maximum = Math.max(0, Number.isFinite(event.focusMaximum) ? event.focusMaximum : 0);
|
|
const current = Math.max(0, Math.min(maximum, Number.isFinite(event.focusCurrent) ? event.focusCurrent : 0));
|
|
const focusAfter = Math.min(maximum, current + mechanics.focusPerProc);
|
|
return Object.freeze({
|
|
focusGain: focusAfter - current,
|
|
focusAfter,
|
|
lastProcAt: event.now,
|
|
});
|
|
}
|