Files
healer-man/src/game/wow335ShamanMaelstromWeaponRuntime.ts
T
2026-10-02 10:38:12 -04:00

112 lines
4.7 KiB
TypeScript

import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
export const WOW335_SHAMAN_MAELSTROM_WEAPON_TALENT_ID = "talent-2057";
export const WOW335_SHAMAN_MAELSTROM_WEAPON_AURA_TAG = "wow335-shaman-maelstrom-weapon";
export const WOW335_SHAMAN_MAELSTROM_WEAPON_DURATION_MS = 30_000;
export const WOW335_SHAMAN_MAELSTROM_WEAPON_MAX_STACKS = 5;
export const WOW335_SHAMAN_MAELSTROM_WEAPON_DEFAULT_SPEED_MS = 2_400;
export const WOW335_SHAMAN_MAELSTROM_WEAPON_ELIGIBLE_SPELLS: ReadonlySet<string> = new Set([
"Lightning Bolt",
"Chain Lightning",
"Lava Burst",
"Lesser Healing Wave",
"Fireball",
"Frostbolt",
]);
const WOW335_SHAMAN_MAELSTROM_WEAPON_RANK_SPELL_IDS = [51528, 51529, 51530] as const;
// The generated rank rows omit proc rates (procChance is 0 for each rank).
// WotLKDB spells 51528, 51529, and 51530 supply 4/8/12 procs per minute.
const WOW335_SHAMAN_MAELSTROM_WEAPON_PROCS_PER_MINUTE = [4, 8, 12] as const;
const WOW335_SHAMAN_MAELSTROM_WEAPON_SOURCE = WOW335_TALENT_NODES.find((node) => (
node.id === WOW335_SHAMAN_MAELSTROM_WEAPON_TALENT_ID
));
export interface Wow335ShamanMaelstromWeaponProfile {
readonly rank: 1 | 2 | 3;
readonly spellId: number;
readonly procsPerMinute: number;
readonly procChancePerWeaponHit: number;
readonly durationMs: number;
readonly maxStacks: number;
readonly castTimeReductionPerStackPercent: number;
readonly pvpDamageReductionPerStackPercent: number;
}
/**
* Resolves the generated Maelstrom Weapon rank and the missing 3.3.5a PPM
* value. WotLKDB child spell 53817 supplies the missing 30-second aura
* duration; the generated rank tooltip remains authoritative for 20% per
* stack and the five-stack cap.
*/
export function wow335ShamanMaelstromWeaponProfile(
rank: number,
weaponSpeedMs = WOW335_SHAMAN_MAELSTROM_WEAPON_DEFAULT_SPEED_MS,
): Wow335ShamanMaelstromWeaponProfile | null {
const safeRank = Math.min(3, Math.max(0, Math.trunc(Number.isFinite(rank) ? rank : 0)));
if (safeRank === 0 || !Number.isFinite(weaponSpeedMs) || weaponSpeedMs <= 0) return null;
const rankDescription = WOW335_SHAMAN_MAELSTROM_WEAPON_SOURCE?.rankDescriptions[safeRank - 1];
const rankEffect = WOW335_SHAMAN_MAELSTROM_WEAPON_SOURCE?.rankEffects[safeRank - 1];
const expectedSpellId = WOW335_SHAMAN_MAELSTROM_WEAPON_RANK_SPELL_IDS[safeRank - 1];
if (!rankDescription || !rankEffect || rankEffect.spellId !== expectedSpellId) return null;
const castTimeReductionPerStackPercent = Number(rankDescription.match(/reduce the cast time .*? by (\d+)%/i)?.[1]);
const maxStacks = Number(rankDescription.match(/Stacks up to (\d+)/i)?.[1]);
const pvpDamageReductionPerStackPercent = Number(
rankDescription.match(/deal (\d+)% reduced damage in PvP combat per stack/i)?.[1],
);
if (!Number.isFinite(castTimeReductionPerStackPercent) || castTimeReductionPerStackPercent <= 0
|| !Number.isFinite(maxStacks) || maxStacks <= 0
|| !Number.isFinite(pvpDamageReductionPerStackPercent) || pvpDamageReductionPerStackPercent < 0) return null;
const procsPerMinute = WOW335_SHAMAN_MAELSTROM_WEAPON_PROCS_PER_MINUTE[safeRank - 1];
return {
rank: safeRank as 1 | 2 | 3,
spellId: expectedSpellId,
procsPerMinute,
// The source supplies a PPM rate, so chance scales with the attacking
// weapon's speed. This simulator models at most one proc per hit.
procChancePerWeaponHit: Math.min(1, procsPerMinute * weaponSpeedMs / 60_000),
durationMs: WOW335_SHAMAN_MAELSTROM_WEAPON_DURATION_MS,
maxStacks,
castTimeReductionPerStackPercent,
pvpDamageReductionPerStackPercent,
};
}
export function wow335ShamanMaelstromWeaponCastTimeReductionPercent(
profile: Pick<Wow335ShamanMaelstromWeaponProfile, "maxStacks" | "castTimeReductionPerStackPercent">,
stacks: number,
): number {
const safeStacks = Math.min(
profile.maxStacks,
Math.max(0, Math.trunc(Number.isFinite(stacks) ? stacks : 0)),
);
return Math.min(100, safeStacks * profile.castTimeReductionPerStackPercent);
}
/**
* Resolves the generated Maelstrom Weapon PvP damage penalty for an eligible
* damage spell. Lesser Healing Wave is eligible for the cast-time proc but is
* a heal, so this damage-only rider does not apply to it.
*/
export function wow335ShamanMaelstromWeaponPvpDamageReductionPercent(
rank: number,
consumedStacks: number,
spellName: string,
): number {
if (!WOW335_SHAMAN_MAELSTROM_WEAPON_ELIGIBLE_SPELLS.has(spellName)
|| spellName === "Lesser Healing Wave") return 0;
const profile = wow335ShamanMaelstromWeaponProfile(rank);
if (!profile) return 0;
const safeStacks = Math.min(
profile.maxStacks,
Math.max(0, Math.trunc(Number.isFinite(consumedStacks) ? consumedStacks : 0)),
);
return safeStacks * profile.pvpDamageReductionPerStackPercent;
}