111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import type { TalentModifier } from "./talentModifiers";
|
|
import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
|
|
|
|
export const WOW335_DRUID_MASTER_SHAPESHIFTER_TALENT_ID = "talent-1915";
|
|
|
|
export type Wow335DruidShapeshiftForm = "bear" | "cat" | "moonkin" | "tree-of-life";
|
|
|
|
const masterShapeshifterTalent = WOW335_TALENT_NODES.find(
|
|
(talent) => talent.id === WOW335_DRUID_MASTER_SHAPESHIFTER_TALENT_ID,
|
|
) ?? (() => {
|
|
throw new Error(`Generated Druid talent ${WOW335_DRUID_MASTER_SHAPESHIFTER_TALENT_ID} is missing`);
|
|
})();
|
|
|
|
function normalizedRank(rank: number): number {
|
|
return Number.isFinite(rank)
|
|
? Math.max(0, Math.min(masterShapeshifterTalent.rankDescriptions.length, Math.trunc(rank)))
|
|
: 0;
|
|
}
|
|
|
|
/**
|
|
* The generated rank tooltip has a separate bonus for each shapeshift form.
|
|
* Return only the active form's modifier so the clauses cannot leak globally.
|
|
*/
|
|
export function wow335DruidMasterShapeshifterModifiersForForm(
|
|
rank: number,
|
|
form: Wow335DruidShapeshiftForm | null,
|
|
): readonly TalentModifier[] {
|
|
const normalized = normalizedRank(rank);
|
|
if (normalized <= 0 || !form) return [];
|
|
const description = masterShapeshifterTalent.rankDescriptions[normalized - 1] ?? "";
|
|
const clauses = {
|
|
bear: {
|
|
pattern: /Bear Form\s*-\s*Increases physical damage by\s+(\d+(?:\.\d+)?)%/i,
|
|
kind: "damage-percent" as const,
|
|
scope: "physical damage",
|
|
},
|
|
cat: {
|
|
pattern: /Cat Form\s*-\s*Increases melee critical strike chance by\s+(\d+(?:\.\d+)?)%/i,
|
|
kind: "critical-chance-percent" as const,
|
|
scope: "melee critical strike chance",
|
|
},
|
|
moonkin: {
|
|
pattern: /Moonkin Form\s*-\s*Increases Arcane and Nature damage by\s+(\d+(?:\.\d+)?)%/i,
|
|
kind: "damage-percent" as const,
|
|
scope: "Arcane and Nature damage",
|
|
},
|
|
"tree-of-life": {
|
|
pattern: /Tree of Life Form\s*-\s*Increases healing by\s+(\d+(?:\.\d+)?)%/i,
|
|
kind: "healing-percent" as const,
|
|
scope: "healing",
|
|
},
|
|
} as const;
|
|
const clause = clauses[form];
|
|
const amount = Number(description.match(clause.pattern)?.[1] ?? 0);
|
|
if (!Number.isFinite(amount) || amount <= 0) return [];
|
|
|
|
return [{
|
|
kind: clause.kind,
|
|
amount,
|
|
unit: "percent",
|
|
scope: clause.scope,
|
|
sourceTalentId: WOW335_DRUID_MASTER_SHAPESHIFTER_TALENT_ID,
|
|
sourceTalentName: "Master Shapeshifter",
|
|
rank: normalized,
|
|
description,
|
|
}];
|
|
}
|
|
|
|
/** Resolve the stock WoW shapeshift spell IDs used by the active aura runtime. */
|
|
export function wow335DruidShapeshiftFormForSpellId(spellId: number): Wow335DruidShapeshiftForm | null {
|
|
switch (spellId) {
|
|
case 5487: // Bear Form
|
|
case 9634: // Dire Bear Form
|
|
return "bear";
|
|
case 768: // Cat Form
|
|
return "cat";
|
|
case 24858: // Moonkin Form
|
|
return "moonkin";
|
|
case 33891: // Tree of Life
|
|
case 65139: // Generated Tree of Life form row
|
|
return "tree-of-life";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Extract a form from the canonical aura IDs written by generated WoW forms. */
|
|
export function wow335DruidShapeshiftFormForAuraId(auraId: string): Wow335DruidShapeshiftForm | null {
|
|
const spellId = auraId.match(/^wow335:(\d+):/)?.[1];
|
|
return spellId === undefined ? null : wow335DruidShapeshiftFormForSpellId(Number(spellId));
|
|
}
|
|
|
|
/** Find an active generated shapeshift aura on the requested actor. */
|
|
export function wow335DruidShapeshiftFormForAuras(
|
|
auras: readonly {
|
|
readonly definition: { readonly id: string };
|
|
readonly targetId: string;
|
|
readonly expiresAt: number | null;
|
|
}[],
|
|
actorId: string,
|
|
now: number,
|
|
): Wow335DruidShapeshiftForm | null {
|
|
const matching = auras
|
|
.filter((aura) => aura.targetId === actorId && (aura.expiresAt === null || aura.expiresAt > now))
|
|
.map((aura) => wow335DruidShapeshiftFormForAuraId(aura.definition.id))
|
|
.filter((form): form is Wow335DruidShapeshiftForm => form !== null);
|
|
// Normally the aura system removes the previous form. Taking the most recent
|
|
// active form also makes stale imported form auras deterministic.
|
|
return matching.at(-1) ?? null;
|
|
}
|