forked from phenom/healer-man
Release Healer Man 0.1.15
This commit is contained in:
1 parent
7b41b102fa
commit
f49e54f35f
43 files changed
+1478
-112
No files matched your search
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
baseClassFor,
|
||||
classById,
|
||||
type ClassId,
|
||||
} from "../app/characterCatalog";
|
||||
import type { ItemStats, NumericItemStatKey } from "./itemStats";
|
||||
import type { PartyRole } from "./partyRoles";
|
||||
|
||||
export type GearArchetype =
|
||||
| "strength-dps"
|
||||
| "agility-dps"
|
||||
| "ranged-dps"
|
||||
| "spell-dps"
|
||||
| "healer"
|
||||
| "block-tank"
|
||||
| "non-block-tank";
|
||||
|
||||
export type WeaponLayout =
|
||||
| "two-hand"
|
||||
| "dual-wield"
|
||||
| "weapon-shield"
|
||||
| "one-hand"
|
||||
| "ranged"
|
||||
| "caster";
|
||||
|
||||
export interface SpecializationDefinition {
|
||||
readonly id: string;
|
||||
readonly classId: ClassId;
|
||||
readonly displayName: string;
|
||||
readonly role: PartyRole;
|
||||
readonly talentTree: string;
|
||||
readonly gearArchetype: GearArchetype;
|
||||
readonly weaponLayout: WeaponLayout;
|
||||
readonly statWeights: Readonly<Partial<Record<NumericItemStatKey, number>>>;
|
||||
}
|
||||
|
||||
const WEIGHTS: Readonly<Record<GearArchetype, Readonly<Partial<Record<NumericItemStatKey, number>>>>> = Object.freeze({
|
||||
"strength-dps": Object.freeze({ strength: 1, stamina: 0.18, attackPower: 0.5, hitRating: 0.82, criticalStrikeRating: 0.72, hasteRating: 0.62, expertiseRating: 0.76, armorPenetrationRating: 0.66 }),
|
||||
"agility-dps": Object.freeze({ agility: 1, stamina: 0.18, attackPower: 0.45, hitRating: 0.82, criticalStrikeRating: 0.78, hasteRating: 0.64, expertiseRating: 0.72, armorPenetrationRating: 0.66 }),
|
||||
"ranged-dps": Object.freeze({ agility: 1, stamina: 0.16, rangedAttackPower: 0.5, hitRating: 0.86, criticalStrikeRating: 0.78, hasteRating: 0.66, armorPenetrationRating: 0.6 }),
|
||||
"spell-dps": Object.freeze({ intellect: 0.8, spirit: 0.28, stamina: 0.14, spellPower: 1, hitRating: 0.86, criticalStrikeRating: 0.68, hasteRating: 0.74 }),
|
||||
healer: Object.freeze({ intellect: 0.82, spirit: 0.62, stamina: 0.16, spellPower: 1, criticalStrikeRating: 0.46, hasteRating: 0.68, mp5: 0.82 }),
|
||||
"block-tank": Object.freeze({ strength: 0.64, stamina: 1, armor: 0.12, defenseRating: 0.86, dodgeRating: 0.7, parryRating: 0.68, blockRating: 0.72, blockValue: 0.66, expertiseRating: 0.38 }),
|
||||
"non-block-tank": Object.freeze({ agility: 0.66, strength: 0.28, stamina: 1, armor: 0.14, defenseRating: 0.78, dodgeRating: 0.86, parryRating: 0.46, expertiseRating: 0.4 }),
|
||||
});
|
||||
|
||||
type SpecTuple = readonly [
|
||||
classId: ClassId,
|
||||
displayName: string,
|
||||
role: PartyRole,
|
||||
gearArchetype: GearArchetype,
|
||||
weaponLayout: WeaponLayout,
|
||||
talentTree?: string,
|
||||
];
|
||||
|
||||
function specId(classId: ClassId, name: string): string {
|
||||
return `${classId}:${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
|
||||
}
|
||||
|
||||
function define(tuple: SpecTuple): SpecializationDefinition {
|
||||
const [classId, displayName, role, gearArchetype, weaponLayout, talentTree = displayName] = tuple;
|
||||
return Object.freeze({
|
||||
id: specId(classId, displayName),
|
||||
classId,
|
||||
displayName,
|
||||
role,
|
||||
talentTree,
|
||||
gearArchetype,
|
||||
weaponLayout,
|
||||
statWeights: WEIGHTS[gearArchetype],
|
||||
});
|
||||
}
|
||||
|
||||
const CLASSIC_SPECIALIZATIONS: readonly SpecTuple[] = [
|
||||
["warrior", "Arms", "damage", "strength-dps", "two-hand"],
|
||||
["warrior", "Fury", "damage", "strength-dps", "dual-wield"],
|
||||
["warrior", "Protection", "tank", "block-tank", "weapon-shield"],
|
||||
["paladin", "Holy", "healer", "healer", "weapon-shield"],
|
||||
["paladin", "Protection", "tank", "block-tank", "weapon-shield"],
|
||||
["paladin", "Retribution", "damage", "strength-dps", "two-hand"],
|
||||
["hunter", "Beast Mastery", "damage", "ranged-dps", "ranged"],
|
||||
["hunter", "Marksmanship", "damage", "ranged-dps", "ranged"],
|
||||
["hunter", "Survival", "damage", "ranged-dps", "ranged"],
|
||||
["rogue", "Assassination", "damage", "agility-dps", "dual-wield"],
|
||||
["rogue", "Combat", "damage", "agility-dps", "dual-wield"],
|
||||
["rogue", "Subtlety", "damage", "agility-dps", "dual-wield"],
|
||||
["priest", "Discipline", "healer", "healer", "caster"],
|
||||
["priest", "Holy", "healer", "healer", "caster"],
|
||||
["priest", "Shadow", "damage", "spell-dps", "caster"],
|
||||
["death-knight", "Blood", "tank", "non-block-tank", "two-hand"],
|
||||
["death-knight", "Frost", "damage", "strength-dps", "dual-wield"],
|
||||
["death-knight", "Unholy", "damage", "strength-dps", "two-hand"],
|
||||
["shaman", "Elemental", "damage", "spell-dps", "weapon-shield"],
|
||||
["shaman", "Enhancement", "damage", "agility-dps", "dual-wield"],
|
||||
["shaman", "Restoration", "healer", "healer", "weapon-shield"],
|
||||
["mage", "Arcane", "damage", "spell-dps", "caster"],
|
||||
["mage", "Fire", "damage", "spell-dps", "caster"],
|
||||
["mage", "Frost", "damage", "spell-dps", "caster"],
|
||||
["warlock", "Affliction", "damage", "spell-dps", "caster"],
|
||||
["warlock", "Demonology", "damage", "spell-dps", "caster"],
|
||||
["warlock", "Destruction", "damage", "spell-dps", "caster"],
|
||||
["druid", "Balance", "damage", "spell-dps", "caster"],
|
||||
["druid", "Feral (Bear)", "tank", "non-block-tank", "two-hand", "Feral Combat"],
|
||||
["druid", "Feral (Cat)", "damage", "agility-dps", "two-hand", "Feral Combat"],
|
||||
["druid", "Restoration", "healer", "healer", "caster"],
|
||||
] as const;
|
||||
|
||||
/** CoA roles are authored explicitly. Support trees map to damage, the closest runtime role. */
|
||||
const COA_SPECIALIZATIONS: readonly SpecTuple[] = [
|
||||
["barbarian", "Brutality", "damage", "agility-dps", "two-hand"], ["barbarian", "Headhunting", "damage", "agility-dps", "ranged"], ["barbarian", "Ancestry", "damage", "agility-dps", "two-hand"],
|
||||
["witch-doctor", "Shadowhunting", "damage", "ranged-dps", "ranged"], ["witch-doctor", "Voodoo", "damage", "spell-dps", "caster"], ["witch-doctor", "Brewing", "healer", "healer", "caster"],
|
||||
["felsworn", "Slayer", "damage", "agility-dps", "dual-wield"], ["felsworn", "Infernal", "tank", "non-block-tank", "two-hand"], ["felsworn", "Tyrant", "damage", "spell-dps", "caster"],
|
||||
["witch-hunter", "Boltslinger", "damage", "ranged-dps", "ranged"], ["witch-hunter", "Houndmaster", "damage", "ranged-dps", "ranged"], ["witch-hunter", "Inquisition", "damage", "agility-dps", "dual-wield"], ["witch-hunter", "Black Knight", "tank", "non-block-tank", "two-hand"],
|
||||
["stormbringer", "Maelstrom", "damage", "spell-dps", "caster"], ["stormbringer", "Lightning", "damage", "spell-dps", "caster"], ["stormbringer", "Wind", "damage", "spell-dps", "caster"],
|
||||
["knight-of-xoroth", "Hellfire", "damage", "strength-dps", "two-hand"], ["knight-of-xoroth", "Defiance", "tank", "block-tank", "weapon-shield"], ["knight-of-xoroth", "War", "damage", "strength-dps", "two-hand"],
|
||||
["guardian", "Gladiator", "damage", "strength-dps", "one-hand"], ["guardian", "Inspiration", "damage", "strength-dps", "weapon-shield"], ["guardian", "Vanguard", "tank", "block-tank", "weapon-shield"],
|
||||
["templar", "Zealot", "damage", "agility-dps", "dual-wield"], ["templar", "Oathkeeper", "tank", "non-block-tank", "two-hand"], ["templar", "Crusader", "damage", "agility-dps", "two-hand"],
|
||||
["bloodmage", "Sanguine", "damage", "agility-dps", "dual-wield"], ["bloodmage", "Accursed", "damage", "spell-dps", "caster"], ["bloodmage", "Eternal", "tank", "non-block-tank", "two-hand"], ["bloodmage", "Fleshweaver", "healer", "healer", "caster"],
|
||||
["ranger", "Archery", "damage", "ranged-dps", "ranged"], ["ranger", "Brigand", "damage", "agility-dps", "dual-wield"], ["ranger", "Farstrider", "damage", "ranged-dps", "ranged"],
|
||||
["chronomancer", "Infinite", "damage", "spell-dps", "caster"], ["chronomancer", "Time", "healer", "healer", "caster"], ["chronomancer", "Artificer", "damage", "spell-dps", "caster"],
|
||||
["necromancer", "Death", "damage", "spell-dps", "caster"], ["necromancer", "Rime", "damage", "spell-dps", "caster"], ["necromancer", "Animation", "damage", "spell-dps", "caster"],
|
||||
["pyromancer", "Incineration", "damage", "spell-dps", "caster"], ["pyromancer", "Flameweaving", "damage", "spell-dps", "caster"], ["pyromancer", "Draconic", "damage", "spell-dps", "caster"],
|
||||
["cultist", "Godblade", "damage", "strength-dps", "two-hand"], ["cultist", "Corruption", "damage", "spell-dps", "caster"], ["cultist", "Heretic", "healer", "healer", "caster"], ["cultist", "Dreadnought", "tank", "block-tank", "weapon-shield"],
|
||||
["starcaller", "Moon Guard", "tank", "block-tank", "weapon-shield"], ["starcaller", "Sentinel", "damage", "ranged-dps", "ranged"], ["starcaller", "Moon Priest", "healer", "healer", "caster"], ["starcaller", "Warden", "damage", "strength-dps", "two-hand"],
|
||||
["sun-cleric", "Piety", "damage", "spell-dps", "caster"], ["sun-cleric", "Blessings", "healer", "healer", "caster"], ["sun-cleric", "Valkyrie", "tank", "block-tank", "weapon-shield"], ["sun-cleric", "Seraphim", "damage", "spell-dps", "caster"],
|
||||
["tinker", "Demolition", "damage", "ranged-dps", "ranged"], ["tinker", "Invention", "healer", "healer", "ranged"], ["tinker", "Mechanics", "damage", "ranged-dps", "ranged"],
|
||||
["venomancer", "Venom", "damage", "spell-dps", "caster"], ["venomancer", "Stalking", "damage", "agility-dps", "dual-wield"], ["venomancer", "Fortitude", "tank", "non-block-tank", "two-hand"], ["venomancer", "Vizier", "healer", "healer", "caster"],
|
||||
["reaper", "Harvest", "damage", "agility-dps", "two-hand"], ["reaper", "Soul", "damage", "spell-dps", "caster"], ["reaper", "Domination", "tank", "non-block-tank", "two-hand"],
|
||||
["primalist", "Primal", "damage", "agility-dps", "two-hand"], ["primalist", "Geomancy", "damage", "spell-dps", "caster"], ["primalist", "Life", "healer", "healer", "caster"], ["primalist", "Mountain King", "tank", "non-block-tank", "two-hand"],
|
||||
["runemaster", "Runic", "damage", "spell-dps", "caster"], ["runemaster", "Arcane", "damage", "spell-dps", "caster"], ["runemaster", "Riftblade", "tank", "non-block-tank", "two-hand"],
|
||||
] as const;
|
||||
|
||||
export const SPECIALIZATION_DEFINITIONS: readonly SpecializationDefinition[] = Object.freeze(
|
||||
[...CLASSIC_SPECIALIZATIONS, ...COA_SPECIALIZATIONS].map(define),
|
||||
);
|
||||
|
||||
const BY_ID = new Map(SPECIALIZATION_DEFINITIONS.map((definition) => [definition.id, definition]));
|
||||
|
||||
export function specializationsForClass(classId: ClassId): readonly SpecializationDefinition[] {
|
||||
return SPECIALIZATION_DEFINITIONS.filter((definition) => definition.classId === classId);
|
||||
}
|
||||
|
||||
export function specializationById(id: string | null | undefined): SpecializationDefinition | null {
|
||||
return id ? BY_ID.get(id) ?? null : null;
|
||||
}
|
||||
|
||||
export function normalizeSpecializationId(classId: ClassId, value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const definition = specializationById(value);
|
||||
return definition?.classId === classId ? definition.id : null;
|
||||
}
|
||||
|
||||
export function specializationScore(stats: ItemStats, definition: SpecializationDefinition): number {
|
||||
let score = 0;
|
||||
for (const [key, weight] of Object.entries(definition.statWeights)) {
|
||||
const value = stats[key as NumericItemStatKey];
|
||||
if (typeof value === "number" && Number.isFinite(value)) score += value * (weight ?? 0);
|
||||
}
|
||||
const melee = stats.meleeWeapon;
|
||||
const ranged = stats.rangedWeapon;
|
||||
const weapon = definition.weaponLayout === "ranged" ? ranged : melee;
|
||||
if (weapon?.damageMax && weapon.speedMs) score += ((weapon.damageMin ?? 0) + weapon.damageMax) * 0.42;
|
||||
return Math.round(score * 100) / 100;
|
||||
}
|
||||
|
||||
/** RoM has no saved specialization; derive a complete gear profile from primary class and activity role. */
|
||||
export function romGearProfile(classId: ClassId, role: PartyRole): SpecializationDefinition {
|
||||
const definition = classById(classId);
|
||||
const base = baseClassFor(classId);
|
||||
const caster = ["priest", "mage", "warlock"].includes(base) || definition.primaryStat === "intellect" || definition.primaryStat === "spirit" || definition.primaryStat === "wisdom";
|
||||
const gearArchetype: GearArchetype = role === "healer"
|
||||
? "healer"
|
||||
: role === "tank"
|
||||
? (["warrior", "paladin"].includes(base) ? "block-tank" : "non-block-tank")
|
||||
: caster ? "spell-dps" : base === "hunter" ? "ranged-dps" : definition.primaryStat === "strength" ? "strength-dps" : "agility-dps";
|
||||
const weaponLayout: WeaponLayout = role === "tank" && gearArchetype === "block-tank"
|
||||
? "weapon-shield"
|
||||
: gearArchetype === "ranged-dps" ? "ranged" : caster ? "caster" : "two-hand";
|
||||
return Object.freeze({
|
||||
id: `${classId}:role-${role}`,
|
||||
classId,
|
||||
displayName: `${definition.name} ${role}`,
|
||||
role,
|
||||
talentTree: "Primary class",
|
||||
gearArchetype,
|
||||
weaponLayout,
|
||||
statWeights: WEIGHTS[gearArchetype],
|
||||
});
|
||||
}
|
||||
|
||||
export function gearProfileFor(
|
||||
classId: ClassId,
|
||||
specializationId: string | null | undefined,
|
||||
fallbackRole: PartyRole,
|
||||
): SpecializationDefinition {
|
||||
return specializationById(normalizeSpecializationId(classId, specializationId))
|
||||
?? romGearProfile(classId, fallbackRole);
|
||||
}
|
||||
Reference in new issue
Block a user