Files
healer-man/src/app/characterCatalog.ts

676 lines
37 KiB
TypeScript

import type { ContentCategoryId } from "./contentCategories";
export type FactionId = "alliance" | "horde" | "taborea";
export type GenderId = "male" | "female";
export type RaceId =
| "human"
| "orc"
| "dwarf"
| "night-elf"
| "undead"
| "tauren"
| "gnome"
| "troll"
| "blood-elf"
| "draenei"
| "goblin"
| "zandalari-troll"
| "kul-tiran"
| "vulpera"
| "pandaren"
| "thin-human"
| "fel-orc"
| "broken"
| "skeleton"
| "vrykul"
| "tuskarr"
| "forest-troll"
| "taunka"
| "northrend-skeleton"
| "ice-troll"
| "earthen"
| "human-cultist"
| "rom-human"
| "rom-elf"
| "rom-dwarf";
export type RomRaceId = "rom-human" | "rom-elf" | "rom-dwarf";
export type BaseClassId =
| "warrior"
| "paladin"
| "hunter"
| "rogue"
| "priest"
| "death-knight"
| "shaman"
| "mage"
| "warlock"
| "druid";
export type CoaClassId =
| "barbarian"
| "witch-doctor"
| "felsworn"
| "witch-hunter"
| "stormbringer"
| "knight-of-xoroth"
| "guardian"
| "templar"
| "bloodmage"
| "ranger"
| "chronomancer"
| "necromancer"
| "pyromancer"
| "cultist"
| "starcaller"
| "sun-cleric"
| "tinker"
| "venomancer"
| "reaper"
| "primalist"
| "runemaster";
export type RomClassId =
| "rom-warrior"
| "rom-scout"
| "rom-rogue"
| "rom-mage"
| "rom-priest"
| "rom-knight"
| "rom-warden"
| "rom-druid"
| "rom-warlock"
| "rom-champion";
export type ClassId = BaseClassId | CoaClassId | RomClassId;
export type ClassArmor = "cloth" | "leather" | "mail" | "chain" | "plate";
export type ClassPrimaryStat = "strength" | "agility" | "intellect" | "wisdom" | "spirit";
export interface CharacterAppearance {
skinColor: number;
face: number;
hairStyle: number;
hairColor: number;
feature: number;
}
export interface AppearanceLabels {
skinColor: string;
face: string;
hairStyle: string;
hairColor: string;
feature: string;
}
export interface AppearanceCounts {
skinColor: number;
face: number;
hairStyle: number;
hairColor: number;
feature: number;
}
export interface RaceDefinition {
id: RaceId;
/** Existing definitions without this field normalize to the WoW category. */
categoryId?: ContentCategoryId;
dbcId: number;
name: string;
faction: FactionId;
/** Omitted for the original two-gender Wrath races. */
genders?: readonly GenderId[];
/** Ascension model races without CharBaseInfo rows use the full app class roster. */
unrestrictedClasses?: boolean;
sigil: string;
accent: string;
description: string;
labels: Record<GenderId, AppearanceLabels>;
counts: Record<GenderId, AppearanceCounts>;
}
export interface ClassDefinition {
id: ClassId;
/** Existing definitions without this field normalize to the WoW category. */
categoryId?: ContentCategoryId;
dbcId: number;
name: string;
role: string;
sigil: string;
color: string;
description: string;
mode: "classic" | "conquest" | "rom";
armor: ClassArmor;
primaryStat: ClassPrimaryStat;
resourceSummary: string;
specializations: readonly string[];
archetype: BaseClassId;
}
const BASE_LABELS: AppearanceLabels = {
skinColor: "Skin Color",
face: "Face",
hairStyle: "Hair Style",
hairColor: "Hair Color",
feature: "Features",
};
function labels(feature: string, overrides: Partial<AppearanceLabels> = {}): AppearanceLabels {
return { ...BASE_LABELS, feature, ...overrides };
}
function counts(
skinColor: number,
face: number,
hairStyle: number,
hairColor: number,
feature: number,
): AppearanceCounts {
return { skinColor, face, hairStyle, hairColor, feature };
}
function ascensionRace(definition: {
id: RaceId;
dbcId: number;
name: string;
faction: FactionId;
genders?: readonly GenderId[];
sigil: string;
accent: string;
description: string;
male: AppearanceCounts;
female?: AppearanceCounts;
featureLabel?: string;
}): RaceDefinition {
const featureLabel = definition.featureLabel ?? "Features";
return {
id: definition.id,
dbcId: definition.dbcId,
name: definition.name,
faction: definition.faction,
genders: definition.genders,
unrestrictedClasses: true,
sigil: definition.sigil,
accent: definition.accent,
description: definition.description,
labels: { male: labels(featureLabel), female: labels(featureLabel) },
counts: { male: definition.male, female: definition.female ?? definition.male },
};
}
/**
* The original ten retain their extracted CharBaseInfo class restrictions.
* Additional Ascension races are model-backed client options; since their
* DBC rows omit class pairs, Healer Man exposes its complete class roster.
*/
export const RACES: readonly RaceDefinition[] = [
{
id: "human", dbcId: 1, name: "Human", faction: "alliance", sigil: "HU", accent: "#6f9dd7",
description: "Versatile champions of the Alliance kingdoms.",
labels: { male: labels("Facial Hair"), female: labels("Piercings") },
counts: { male: counts(10, 12, 17, 10, 9), female: counts(10, 15, 24, 10, 7) },
},
{
id: "orc", dbcId: 2, name: "Orc", faction: "horde", sigil: "OR", accent: "#bd6450",
description: "Unyielding warriors bound by strength and honor.",
labels: { male: labels("Facial Hair"), female: labels("Piercings") },
counts: { male: counts(9, 9, 12, 8, 11), female: counts(9, 9, 13, 8, 7) },
},
{
id: "dwarf", dbcId: 3, name: "Dwarf", faction: "alliance", sigil: "DW", accent: "#c58c5a",
description: "Hardy mountain folk, explorers, and steadfast allies.",
labels: { male: labels("Facial Hair"), female: labels("Piercings") },
counts: { male: counts(9, 10, 16, 10, 11), female: counts(9, 10, 19, 10, 6) },
},
{
id: "night-elf", dbcId: 4, name: "Night Elf", faction: "alliance", sigil: "NE", accent: "#9275cf",
description: "Ancient guardians attuned to shadow and nature.",
labels: { male: labels("Facial Hair"), female: labels("Markings") },
counts: { male: counts(9, 9, 12, 8, 6), female: counts(9, 9, 12, 8, 10) },
},
{
id: "undead", dbcId: 5, name: "Undead", faction: "horde", sigil: "UD", accent: "#8ba39d",
description: "Forsaken survivors wielding grim determination.",
labels: { male: labels("Features"), female: labels("Features") },
counts: { male: counts(6, 10, 15, 10, 17), female: counts(6, 10, 15, 10, 8) },
},
{
id: "tauren", dbcId: 6, name: "Tauren", faction: "horde", sigil: "TA", accent: "#ad805c",
description: "Powerful nomads guided by the Earth Mother.",
labels: {
male: labels("Facial Hair", { hairStyle: "Horn Style", hairColor: "Horn Color" }),
female: labels("Hair", { hairStyle: "Horn Style", hairColor: "Horn Color" }),
},
counts: { male: counts(19, 5, 13, 3, 7), female: counts(11, 4, 12, 3, 5) },
},
{
id: "gnome", dbcId: 7, name: "Gnome", faction: "alliance", sigil: "GN", accent: "#d28faf",
description: "Brilliant inventors with courage beyond their stature.",
labels: { male: labels("Facial Hair"), female: labels("Earrings") },
counts: { male: counts(5, 7, 12, 9, 8), female: counts(5, 7, 12, 9, 7) },
},
{
id: "troll", dbcId: 8, name: "Troll", faction: "horde", sigil: "TR", accent: "#50a99a",
description: "Fierce jungle hunters carrying an ancient legacy.",
labels: { male: labels("Tusks"), female: labels("Tusks") },
counts: { male: counts(6, 5, 10, 10, 11), female: counts(6, 6, 10, 10, 6) },
},
{
id: "blood-elf", dbcId: 10, name: "Blood Elf", faction: "horde", sigil: "BE", accent: "#d75f76",
description: "Arcane survivors rebuilding a proud magical dynasty.",
labels: { male: labels("Facial Hair"), female: labels("Earrings") },
counts: { male: counts(10, 10, 16, 10, 10), female: counts(10, 10, 20, 10, 11) },
},
{
id: "draenei", dbcId: 11, name: "Draenei", faction: "alliance", sigil: "DR", accent: "#6986d8",
description: "Exiled voyagers devoted to hope and the Light.",
labels: { male: labels("Facial Hair"), female: labels("Horn Style") },
counts: { male: counts(14, 10, 14, 7, 8), female: counts(12, 10, 16, 7, 7) },
},
ascensionRace({
id: "goblin", dbcId: 9, name: "Goblin", faction: "horde", sigil: "GO", accent: "#84ad4b",
description: "Cunning cartels turn invention, nerve, and explosive ambition into power.",
male: counts(16, 5, 19, 9, 24), female: counts(9, 10, 18, 9, 24),
}),
ascensionRace({
id: "zandalari-troll", dbcId: 12, name: "Zandalari Troll", faction: "horde", sigil: "ZT", accent: "#d6b763",
description: "Regal troll champions carry the blessings of ancient loa.",
male: counts(24, 6, 12, 16, 24), female: counts(24, 6, 10, 16, 24),
}),
ascensionRace({
id: "kul-tiran", dbcId: 16, name: "Kul Tiran", faction: "alliance", sigil: "KT", accent: "#648aa5",
description: "Storm-tested mariners shaped by the wild seas of Kul Tiras.",
male: counts(6, 7, 6, 13, 24), female: counts(6, 7, 6, 13, 24),
}),
ascensionRace({
id: "vulpera", dbcId: 19, name: "Vulpera", faction: "horde", sigil: "VU", accent: "#d19052",
description: "Resourceful nomads who turn every scrap and hard road into an advantage.",
male: counts(24, 6, 4, 15, 24), female: counts(24, 6, 4, 15, 24),
}),
ascensionRace({
id: "pandaren", dbcId: 20, name: "Pandaren", faction: "alliance", sigil: "PA", accent: "#c99b59",
description: "Disciplined wanderers balancing martial tradition, curiosity, and joy.",
male: counts(17, 20, 22, 16, 24), female: counts(17, 20, 15, 15, 12),
}),
ascensionRace({
id: "thin-human", dbcId: 33, name: "Thin Human", faction: "alliance", genders: ["male"], sigil: "TH", accent: "#9ea6ba",
description: "A wiry human variant recovered from Ascension's extended character models.",
male: counts(6, 1, 4, 7, 7),
}),
ascensionRace({
id: "fel-orc", dbcId: 34, name: "Fel Orc", faction: "horde", sigil: "FO", accent: "#b74235",
description: "Fel-scarred orcs who wield demonic strength at terrible cost.",
male: counts(3, 1, 1, 1, 1), female: counts(1, 1, 1, 1, 1),
}),
ascensionRace({
id: "broken", dbcId: 36, name: "Broken", faction: "alliance", genders: ["male"], sigil: "BR", accent: "#7781a8",
description: "Draenei survivors hardened by exile, corruption, and stubborn hope.",
male: counts(6, 1, 3, 10, 1), female: counts(1, 1, 1, 1, 1),
}),
ascensionRace({
id: "skeleton", dbcId: 37, name: "Skeleton", faction: "horde", genders: ["male"], sigil: "SK", accent: "#b9b59e",
description: "Restless bones animated by will, sorcery, and unfinished purpose.",
male: counts(6, 1, 1, 1, 1), female: counts(1, 1, 1, 1, 1),
}),
ascensionRace({
id: "vrykul", dbcId: 38, name: "Vrykul", faction: "alliance", genders: ["male"], sigil: "VR", accent: "#879db6",
description: "Towering northern warriors bound to saga, steel, and ancient oaths.",
male: counts(6, 1, 6, 5, 6),
}),
ascensionRace({
id: "tuskarr", dbcId: 39, name: "Tuskarr", faction: "alliance", genders: ["male"], sigil: "TU", accent: "#9b765b",
description: "Warm-hearted coastal hunters who endure the frozen north together.",
male: counts(7, 1, 6, 7, 7),
}),
ascensionRace({
id: "forest-troll", dbcId: 40, name: "Forest Troll", faction: "horde", genders: ["male"], sigil: "FT", accent: "#56896c",
description: "Massive forest-born trolls carrying the old strength of their tribes.",
male: counts(6, 5, 5, 10, 11), featureLabel: "Tusks",
}),
ascensionRace({
id: "taunka", dbcId: 41, name: "Taunka", faction: "horde", genders: ["male"], sigil: "TK", accent: "#8f664c",
description: "Northern kin of the tauren who command the land through sheer resolve.",
male: counts(4, 1, 1, 1, 3),
}),
ascensionRace({
id: "northrend-skeleton", dbcId: 42, name: "Northrend Skeleton", faction: "horde", genders: ["male"], sigil: "NS", accent: "#8fa6aa",
description: "Frost-worn undead raised from the forgotten battlefields of Northrend.",
male: counts(4, 1, 16, 4, 10),
}),
ascensionRace({
id: "ice-troll", dbcId: 43, name: "Ice Troll", faction: "horde", genders: ["male"], sigil: "IT", accent: "#608ca8",
description: "Savage frostland trolls adapted to the coldest reaches of Azeroth.",
male: counts(5, 1, 4, 6, 5), featureLabel: "Tusks",
}),
ascensionRace({
id: "earthen", dbcId: 62, name: "Earthen", faction: "alliance", sigil: "EA", accent: "#a87e4f",
description: "Titan-forged defenders shaped from stone, metal, and living memory.",
male: counts(14, 20, 16, 10, 11), female: counts(11, 10, 19, 10, 6),
}),
ascensionRace({
id: "human-cultist", dbcId: 63, name: "Human Cultist", faction: "horde", sigil: "HC", accent: "#816493",
description: "Forbidden devotees marked by shadowed rites and dangerous conviction.",
male: counts(11, 12, 18, 10, 1), female: counts(11, 15, 24, 10, 1),
}),
{
id: "rom-human", categoryId: "rom", dbcId: 1, name: "Human", faction: "taborea", sigil: "RH", accent: "#c99963",
description: "Adaptable Taborean adventurers able to master martial and magical callings.",
labels: { male: labels("Facial Hair"), female: labels("Features") },
counts: { male: counts(8, 8, 10, 8, 8), female: counts(8, 8, 10, 8, 8) },
},
{
id: "rom-elf", categoryId: "rom", dbcId: 2, name: "Elf", faction: "taborea", sigil: "EL", accent: "#79b991",
description: "Ancient children of Taborea attuned to nature, wardenship, and arcane power.",
labels: { male: labels("Features"), female: labels("Features") },
counts: { male: counts(8, 8, 10, 8, 8), female: counts(8, 8, 10, 8, 8) },
},
{
id: "rom-dwarf", categoryId: "rom", dbcId: 3, name: "Dwarf", faction: "taborea", genders: ["male"], sigil: "RD", accent: "#b7794d",
description: "Forge-born warriors whose RuneWaker source rig is authentically male-only.",
labels: { male: labels("Facial Hair"), female: labels("Facial Hair") },
counts: { male: counts(8, 8, 10, 8, 8), female: counts(1, 1, 1, 1, 1) },
},
] as const;
/**
* Rigs approved for normal play. Extended Ascension variants remain in the
* extracted catalog for saved-character compatibility and focused validation,
* but are not offered or generated until their complete presentation passes
* live review. Worgen can join this list once its rig is actually shipped.
*/
const PLAYABLE_RACE_IDS: ReadonlySet<RaceId> = new Set([
"human",
"orc",
"dwarf",
"night-elf",
"undead",
"tauren",
"gnome",
"troll",
"blood-elf",
"draenei",
"goblin",
"rom-human",
"rom-elf",
"rom-dwarf",
]);
export const PLAYABLE_RACES: readonly RaceDefinition[] = RACES.filter(
(race) => PLAYABLE_RACE_IDS.has(race.id),
);
export const CLASSES: readonly ClassDefinition[] = [
{ id: "warrior", dbcId: 1, name: "Warrior", role: "Tank / Damage", sigil: "WA", color: "#c79c6e", description: "Weapon master who thrives in the heart of battle.", mode: "classic", armor: "plate", primaryStat: "strength", resourceSummary: "Rage", specializations: ["Arms", "Fury", "Protection"], archetype: "warrior" },
{ id: "paladin", dbcId: 2, name: "Paladin", role: "Tank / Healer / Damage", sigil: "PA", color: "#f58cba", description: "Holy knight who protects allies with steel and Light.", mode: "classic", armor: "plate", primaryStat: "strength", resourceSummary: "Mana", specializations: ["Holy", "Protection", "Retribution"], archetype: "paladin" },
{ id: "hunter", dbcId: 3, name: "Hunter", role: "Damage", sigil: "HU", color: "#abd473", description: "Ranged tracker who fights beside a loyal beast.", mode: "classic", armor: "mail", primaryStat: "agility", resourceSummary: "Mana", specializations: ["Beast Mastery", "Marksmanship", "Survival"], archetype: "hunter" },
{ id: "rogue", dbcId: 4, name: "Rogue", role: "Damage", sigil: "RO", color: "#fff569", description: "Elusive skirmisher specializing in precise attacks.", mode: "classic", armor: "leather", primaryStat: "agility", resourceSummary: "Energy", specializations: ["Assassination", "Combat", "Subtlety"], archetype: "rogue" },
{ id: "priest", dbcId: 5, name: "Priest", role: "Healer / Damage", sigil: "PR", color: "#f2f2f2", description: "Devout spellcaster who mends wounds or bends shadow.", mode: "classic", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Discipline", "Holy", "Shadow"], archetype: "priest" },
{ id: "death-knight", dbcId: 6, name: "Death Knight", role: "Tank / Damage", sigil: "DK", color: "#c41f3b", description: "Dark hero wielding runes, frost, and forbidden power.", mode: "classic", armor: "plate", primaryStat: "strength", resourceSummary: "Runic Power", specializations: ["Blood", "Frost", "Unholy"], archetype: "death-knight" },
{ id: "shaman", dbcId: 7, name: "Shaman", role: "Healer / Damage", sigil: "SH", color: "#0070de", description: "Elemental guide who calls spirits and restorative rain.", mode: "classic", armor: "mail", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Elemental", "Enhancement", "Restoration"], archetype: "shaman" },
{ id: "mage", dbcId: 8, name: "Mage", role: "Damage", sigil: "MA", color: "#69ccf0", description: "Scholar of arcane, fire, and frost magic.", mode: "classic", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Arcane", "Fire", "Frost"], archetype: "mage" },
{ id: "warlock", dbcId: 9, name: "Warlock", role: "Damage", sigil: "WL", color: "#9482c9", description: "Fel caster commanding curses and summoned demons.", mode: "classic", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Affliction", "Demonology", "Destruction"], archetype: "warlock" },
{ id: "druid", dbcId: 11, name: "Druid", role: "Tank / Healer / Damage", sigil: "DU", color: "#ff7d0a", description: "Shapeshifter drawing power from the wilds.", mode: "classic", armor: "leather", primaryStat: "agility", resourceSummary: "Mana", specializations: ["Balance", "Feral", "Restoration"], archetype: "druid" },
{ id: "barbarian", categoryId: "coa", dbcId: 12, name: "Barbarian", role: "Damage / Support", sigil: "BA", color: "#8a3303", description: "A brutal melee combatant who cleaves foes and rallies ancestral power.", mode: "conquest", armor: "leather", primaryStat: "agility", resourceSummary: "Energy", specializations: ["Brutality", "Headhunting", "Ancestry"], archetype: "rogue" },
{ id: "witch-doctor", categoryId: "coa", dbcId: 13, name: "Witch Doctor", role: "Healer / Damage", sigil: "WD", color: "#6eff00", description: "A shadowhunter who mixes hexes, brews, wards, and restorative voodoo.", mode: "conquest", armor: "leather", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Shadowhunting", "Voodoo", "Brewing"], archetype: "shaman" },
{ id: "felsworn", categoryId: "coa", dbcId: 14, name: "Felsworn", role: "Damage / Tank", sigil: "FE", color: "#a330c9", description: "A demon-touched fighter who turns fel fury into relentless offense and defense.", mode: "conquest", armor: "leather", primaryStat: "agility", resourceSummary: "Rage / Felfury", specializations: ["Slayer", "Infernal", "Tyrant"], archetype: "rogue" },
{ id: "witch-hunter", categoryId: "coa", dbcId: 15, name: "Witch Hunter", role: "Damage / Tank", sigil: "WH", color: "#abd473", description: "An agile inquisitor wielding blades, firearms, traps, and forbidden shadow.", mode: "conquest", armor: "leather", primaryStat: "agility", resourceSummary: "Rage / Mana", specializations: ["Boltslinger", "Houndmaster", "Inquisition", "Black Knight"], archetype: "hunter" },
{ id: "stormbringer", categoryId: "coa", dbcId: 16, name: "Stormbringer", role: "Damage / Support", sigil: "ST", color: "#0070de", description: "An elemental conduit who commands lightning, wind, and violent storms.", mode: "conquest", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana / Static", specializations: ["Maelstrom", "Lightning", "Wind"], archetype: "shaman" },
{ id: "knight-of-xoroth", categoryId: "coa", dbcId: 17, name: "Knight of Xoroth", role: "Tank / Damage", sigil: "KX", color: "#d64747", description: "A demonic plate fighter fueled by rage, deathfire, and hellish summons.", mode: "conquest", armor: "plate", primaryStat: "strength", resourceSummary: "Rage / Deathfire", specializations: ["Hellfire", "Defiance", "War"], archetype: "death-knight" },
{ id: "guardian", categoryId: "coa", dbcId: 18, name: "Guardian", role: "Tank / Damage / Support", sigil: "GU", color: "#c79c6e", description: "A sword-and-board leader who blocks attacks and inspires allies with banners.", mode: "conquest", armor: "plate", primaryStat: "strength", resourceSummary: "Energy", specializations: ["Gladiator", "Inspiration", "Vanguard"], archetype: "warrior" },
{ id: "templar", categoryId: "coa", dbcId: 19, name: "Templar", role: "Damage / Tank", sigil: "TE", color: "#00ff99", description: "A flowing holy martial artist who chains strikes into powerful combos.", mode: "conquest", armor: "leather", primaryStat: "agility", resourceSummary: "Energy / Holy Runes", specializations: ["Zealot", "Oathkeeper", "Crusader"], archetype: "rogue" },
{ id: "bloodmage", categoryId: "coa", dbcId: 20, name: "Bloodmage", role: "Damage / Healer / Tank", sigil: "BM", color: "#cc9900", description: "A sanguine spellblade who spends vitality and embraces the worgen curse.", mode: "conquest", armor: "leather", primaryStat: "agility", resourceSummary: "Rage / Health", specializations: ["Sanguine", "Accursed", "Eternal", "Fleshweaver"], archetype: "druid" },
{ id: "ranger", categoryId: "coa", dbcId: 21, name: "Ranger", role: "Damage / Support", sigil: "RA", color: "#fff569", description: "A mobile skirmisher equally dangerous with bows or paired melee weapons.", mode: "conquest", armor: "leather", primaryStat: "agility", resourceSummary: "Focus / Archery Points", specializations: ["Archery", "Brigand", "Farstrider"], archetype: "hunter" },
{ id: "chronomancer", categoryId: "coa", dbcId: 22, name: "Chronomancer", role: "Healer / Damage", sigil: "CH", color: "#f2e699", description: "A rule-bending caster who reverses wounds and warps order, chaos, and time.", mode: "conquest", armor: "cloth", primaryStat: "spirit", resourceSummary: "Mana", specializations: ["Infinite", "Time", "Artificer"], archetype: "priest" },
{ id: "necromancer", categoryId: "coa", dbcId: 23, name: "Necromancer", role: "Damage", sigil: "NE", color: "#8787ed", description: "A master of frost, plague, and permanent undead minions.", mode: "conquest", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana / Runic Power", specializations: ["Death", "Rime", "Animation"], archetype: "warlock" },
{ id: "pyromancer", categoryId: "coa", dbcId: 24, name: "Pyromancer", role: "Damage / Support", sigil: "PY", color: "#ff300f", description: "A fire specialist who incinerates foes, cauterizes allies, and channels dragons.", mode: "conquest", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Incineration", "Flameweaving", "Draconic"], archetype: "mage" },
{ id: "cultist", categoryId: "coa", dbcId: 25, name: "Cultist", role: "Healer / Damage / Tank", sigil: "CU", color: "#e0c7ff", description: "A versatile servant of the Old Gods wielding eldritch spells and void blades.", mode: "conquest", armor: "plate", primaryStat: "strength", resourceSummary: "Mana / Insanity", specializations: ["Godblade", "Corruption", "Heretic", "Dreadnought"], archetype: "paladin" },
{ id: "starcaller", categoryId: "coa", dbcId: 26, name: "Starcaller", role: "Tank / Damage / Healer", sigil: "SC", color: "#b5ffff", description: "An astral warrior who invokes Elune through blades, bows, and lunar magic.", mode: "conquest", armor: "plate", primaryStat: "intellect", resourceSummary: "Mana", specializations: ["Moon Guard", "Sentinel", "Moon Priest", "Warden"], archetype: "paladin" },
{ id: "sun-cleric", categoryId: "coa", dbcId: 27, name: "Sun Cleric", role: "Damage / Healer / Tank", sigil: "SU", color: "#f58cba", description: "A solar champion who heals with warmth and burns enemies with holy fire.", mode: "conquest", armor: "plate", primaryStat: "intellect", resourceSummary: "Mana / Rage / Solar Power", specializations: ["Piety", "Blessings", "Valkyrie", "Seraphim"], archetype: "paladin" },
{ id: "tinker", categoryId: "coa", dbcId: 28, name: "Tinker", role: "Damage / Healer", sigil: "TI", color: "#a3a3a3", description: "An inventor armed with guns, ammunition, turrets, gadgets, and mechanical allies.", mode: "conquest", armor: "mail", primaryStat: "agility", resourceSummary: "Energy / Ammunition", specializations: ["Demolition", "Invention", "Mechanics"], archetype: "hunter" },
{ id: "venomancer", categoryId: "coa", dbcId: 29, name: "Venomancer", role: "Tank / Damage / Healer", sigil: "VE", color: "#ff7d0a", description: "A shapeshifting toxin master who spreads venom and hardens into insect forms.", mode: "conquest", armor: "mail", primaryStat: "intellect", resourceSummary: "Mana / Rage", specializations: ["Venom", "Stalking", "Fortitude", "Vizier"], archetype: "shaman" },
{ id: "reaper", categoryId: "coa", dbcId: 30, name: "Reaper", role: "Damage / Tank", sigil: "RE", color: "#c41f3b", description: "A soul-harvesting warrior who moves like a spectre and dominates the dead.", mode: "conquest", armor: "mail", primaryStat: "agility", resourceSummary: "Mana / Souls", specializations: ["Harvest", "Soul", "Domination"], archetype: "death-knight" },
{ id: "primalist", categoryId: "coa", dbcId: 31, name: "Primalist", role: "Damage / Tank / Healer", sigil: "PR", color: "#0d2ed6", description: "A primal shapeshifter who calls beasts, stone, magma, and the Earthmother.", mode: "conquest", armor: "mail", primaryStat: "agility", resourceSummary: "Rage / Mana", specializations: ["Primal", "Geomancy", "Life", "Mountain King"], archetype: "druid" },
{ id: "runemaster", categoryId: "coa", dbcId: 32, name: "Runemaster", role: "Damage / Tank", sigil: "RU", color: "#40c7eb", description: "A rune-inscribing battlemage who bends elements, portals, and spellblades.", mode: "conquest", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana / Runes", specializations: ["Runic", "Arcane", "Riftblade"], archetype: "mage" },
{ id: "rom-warrior", categoryId: "rom", dbcId: 1, name: "Warrior", role: "Tank / Damage", sigil: "RW", color: "#b86b3f", description: "A hardened weapon fighter who converts incoming pressure into rage.", mode: "rom", armor: "plate", primaryStat: "strength", resourceSummary: "Rage", specializations: [], archetype: "warrior" },
{ id: "rom-scout", categoryId: "rom", dbcId: 2, name: "Scout", role: "Damage", sigil: "SC", color: "#8bae58", description: "A mobile ranged hunter who relies on focus, precision, and battlefield control.", mode: "rom", armor: "leather", primaryStat: "agility", resourceSummary: "Focus", specializations: [], archetype: "hunter" },
{ id: "rom-rogue", categoryId: "rom", dbcId: 3, name: "Rogue", role: "Damage", sigil: "RR", color: "#dccb55", description: "A quick melee combatant who spends energy on poisons and decisive attacks.", mode: "rom", armor: "leather", primaryStat: "agility", resourceSummary: "Energy", specializations: [], archetype: "rogue" },
{ id: "rom-mage", categoryId: "rom", dbcId: 4, name: "Mage", role: "Damage", sigil: "RM", color: "#6cc7e9", description: "A master of elemental and arcane spellcraft.", mode: "rom", armor: "cloth", primaryStat: "intellect", resourceSummary: "Mana", specializations: [], archetype: "mage" },
{ id: "rom-priest", categoryId: "rom", dbcId: 5, name: "Priest", role: "Healer / Damage", sigil: "RP", color: "#eeeeee", description: "A divine caster who restores allies and punishes enemies with holy power.", mode: "rom", armor: "cloth", primaryStat: "spirit", resourceSummary: "Mana", specializations: [], archetype: "priest" },
{ id: "rom-knight", categoryId: "rom", dbcId: 6, name: "Knight", role: "Tank / Damage", sigil: "RK", color: "#d99cb6", description: "An armored protector who binds defensive discipline to sacred magic.", mode: "rom", armor: "plate", primaryStat: "strength", resourceSummary: "Mana", specializations: [], archetype: "paladin" },
{ id: "rom-warden", categoryId: "rom", dbcId: 7, name: "Warden", role: "Tank / Damage", sigil: "WN", color: "#4eac68", description: "An elven guardian who fights beside summoned natural companions.", mode: "rom", armor: "chain", primaryStat: "strength", resourceSummary: "Mana", specializations: [], archetype: "druid" },
{ id: "rom-druid", categoryId: "rom", dbcId: 8, name: "Druid", role: "Healer / Damage", sigil: "RD", color: "#d98a35", description: "An elven keeper of natural magic, restoration, and storms.", mode: "rom", armor: "cloth", primaryStat: "wisdom", resourceSummary: "Mana", specializations: [], archetype: "druid" },
{ id: "rom-warlock", categoryId: "rom", dbcId: 9, name: "Warlock", role: "Damage", sigil: "RL", color: "#8b72c6", description: "A dwarven occultist who commands psychic pressure and dark focus.", mode: "rom", armor: "cloth", primaryStat: "intellect", resourceSummary: "Focus", specializations: [], archetype: "warlock" },
{ id: "rom-champion", categoryId: "rom", dbcId: 10, name: "Champion", role: "Tank / Damage", sigil: "RC", color: "#9c5f43", description: "A dwarven construct warrior who builds rage and converts it into runic force.", mode: "rom", armor: "chain", primaryStat: "strength", resourceSummary: "Rage", specializations: [], archetype: "warrior" },
] as const;
export const BASE_CLASS_IDS: readonly BaseClassId[] = [
"warrior", "paladin", "hunter", "rogue", "priest",
"death-knight", "shaman", "mage", "warlock", "druid",
] as const;
export const COA_CLASS_IDS: readonly CoaClassId[] = [
"barbarian", "witch-doctor", "felsworn", "witch-hunter", "stormbringer",
"knight-of-xoroth", "guardian", "templar", "bloodmage", "ranger",
"chronomancer", "necromancer", "pyromancer", "cultist", "starcaller",
"sun-cleric", "tinker", "venomancer", "reaper", "primalist", "runemaster",
] as const;
export const ROM_CLASS_IDS: readonly RomClassId[] = [
"rom-warrior", "rom-scout", "rom-rogue", "rom-mage", "rom-priest",
"rom-knight", "rom-warden", "rom-druid", "rom-warlock", "rom-champion",
] as const;
export const CLASS_ICON_BASENAME_BY_ID: Readonly<Record<ClassId, string>> = {
warrior: "classicon128_warrior",
paladin: "classicon128_paladin",
hunter: "classicon128_hunter",
rogue: "classicon128_rogue",
priest: "classicon128_priest",
"death-knight": "classicon128_deathknight",
shaman: "classicon128_shaman",
mage: "classicon128_mage",
warlock: "classicon128_warlock",
druid: "classicon128_druid",
barbarian: "classicon128_barbarian",
"witch-doctor": "classicon128_witchdoctor",
felsworn: "classicon128_demonhunter",
"witch-hunter": "classicon128_witchhunter",
stormbringer: "classicon128_stormbringer",
"knight-of-xoroth": "classicon128_fleshwarden",
guardian: "classicon128_guardian",
templar: "classicon128_monk",
bloodmage: "classicon128_sonofarugal",
ranger: "classicon128_ranger",
chronomancer: "classicon128_chronomancer",
necromancer: "classicon128_necromancer",
pyromancer: "classicon128_pyromancer",
cultist: "classicon128_cultist",
starcaller: "classicon128_starcaller",
"sun-cleric": "classicon128_suncleric",
tinker: "classicon128_tinker",
venomancer: "classicon128_prophet",
reaper: "classicon128_reaper",
primalist: "classicon128_wildwalker",
runemaster: "classicon128_spiritmage",
"rom-warrior": "classicon128_warrior",
"rom-scout": "classicon128_hunter",
"rom-rogue": "classicon128_rogue",
"rom-mage": "classicon128_mage",
"rom-priest": "classicon128_priest",
"rom-knight": "classicon128_paladin",
"rom-warden": "classicon128_guardian",
"rom-druid": "classicon128_druid",
"rom-warlock": "classicon128_warlock",
"rom-champion": "classicon128_spiritmage",
};
const RACE_DBC_IDS_BY_CLASS: Readonly<Record<ClassId, readonly number[]>> = {
warrior: [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
paladin: [1, 3, 5, 6, 10, 11],
hunter: [1, 2, 3, 4, 5, 6, 8, 10, 11],
rogue: [1, 2, 3, 4, 5, 7, 8, 10],
priest: [1, 3, 4, 5, 7, 8, 10, 11],
"death-knight": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
shaman: [2, 3, 6, 8, 11],
mage: [1, 2, 3, 4, 5, 7, 8, 10, 11],
warlock: [1, 2, 5, 7, 8, 10],
druid: [4, 6, 8],
barbarian: [1, 2, 3, 4, 6, 8, 11],
"witch-doctor": [1, 2, 8],
felsworn: [2, 4, 10, 11],
"witch-hunter": [1, 4, 5, 7, 10],
stormbringer: [1, 2, 3, 5, 6, 7, 8, 10, 11],
"knight-of-xoroth": [2, 5, 11],
guardian: [1, 2, 3, 4, 5, 6, 8, 10, 11],
templar: [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
bloodmage: [1, 4, 8, 10],
ranger: [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
chronomancer: [1, 3, 5, 7, 10, 11],
necromancer: [1, 2, 3, 5, 7, 8, 10, 11],
pyromancer: [1, 2, 3, 5, 6, 7, 8, 10],
cultist: [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
starcaller: [4, 6, 10, 11],
"sun-cleric": [1, 3, 5, 6, 7, 8, 10, 11],
tinker: [1, 2, 3, 5, 7, 10, 11],
venomancer: [4, 5, 8],
reaper: [1, 4, 5, 7, 10],
primalist: [2, 3, 4, 6, 8, 11],
runemaster: [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
"rom-warrior": [],
"rom-scout": [],
"rom-rogue": [],
"rom-mage": [],
"rom-priest": [],
"rom-knight": [],
"rom-warden": [],
"rom-druid": [],
"rom-warlock": [],
"rom-champion": [],
};
const ROM_CLASSES_BY_RACE: Readonly<Record<RomRaceId, readonly RomClassId[]>> = {
"rom-human": ["rom-warrior", "rom-scout", "rom-rogue", "rom-mage", "rom-priest", "rom-knight"],
"rom-elf": ["rom-warrior", "rom-scout", "rom-rogue", "rom-mage", "rom-warden", "rom-druid"],
"rom-dwarf": ["rom-warrior", "rom-rogue", "rom-mage", "rom-priest", "rom-warlock", "rom-champion"],
};
export const FACTIONS: Readonly<Record<FactionId, { name: string; motto: string }>> = {
alliance: { name: "Alliance", motto: "For the Alliance" },
horde: { name: "Horde", motto: "For the Horde" },
taborea: { name: "Taborea", motto: "For Taborea" },
};
export const APPEARANCE_KEYS = ["skinColor", "face", "hairStyle", "hairColor", "feature"] as const;
export type AppearanceKey = (typeof APPEARANCE_KEYS)[number];
export function raceById(id: RaceId): RaceDefinition {
return RACES.find((race) => race.id === id) ?? RACES[0];
}
export function classById(id: ClassId): ClassDefinition {
return CLASSES.find((characterClass) => characterClass.id === id) ?? CLASSES[0];
}
export function classIconUrl(id: ClassId): string {
return `/assets/ui/spells/${CLASS_ICON_BASENAME_BY_ID[id]}.png`;
}
export function baseClassFor(id: ClassId): BaseClassId {
return classById(id).archetype;
}
export function contentCategoryForRace(raceId: RaceId): ContentCategoryId {
return raceById(raceId).categoryId ?? "wow";
}
export function contentCategoryForClass(classId: ClassId): ContentCategoryId {
return classById(classId).categoryId ?? "wow";
}
export function isRomRaceId(raceId: RaceId): raceId is RomRaceId {
return contentCategoryForRace(raceId) === "rom";
}
export function isRomClassId(classId: ClassId): classId is RomClassId {
return contentCategoryForClass(classId) === "rom";
}
export function racesForCategory(categoryId: ContentCategoryId): readonly RaceDefinition[] {
return PLAYABLE_RACES.filter((race) => {
const raceCategory = race.categoryId ?? "wow";
return raceCategory === categoryId || (categoryId === "coa" && raceCategory === "wow");
});
}
export function raceSupportsCategory(raceId: RaceId, categoryId: ContentCategoryId): boolean {
const raceCategory = contentCategoryForRace(raceId);
return raceCategory === categoryId || (categoryId === "coa" && raceCategory === "wow");
}
export function classesForRace(
raceId: RaceId,
categoryId?: ContentCategoryId,
): readonly ClassDefinition[] {
const race = raceById(raceId);
if (isRomRaceId(raceId)) {
if (categoryId && categoryId !== "rom") return [];
const allowed = new Set(ROM_CLASSES_BY_RACE[raceId]);
return CLASSES.filter((characterClass) => allowed.has(characterClass.id as RomClassId));
}
if (categoryId === "rom") return [];
const classMatchesCategory = (characterClass: ClassDefinition) => (
categoryId ? contentCategoryForClass(characterClass.id) === categoryId : contentCategoryForClass(characterClass.id) !== "rom"
);
if (race.unrestrictedClasses) return CLASSES.filter((characterClass) => (
classMatchesCategory(characterClass)
));
const raceDbcId = race.dbcId;
return CLASSES.filter((characterClass) => (
classMatchesCategory(characterClass)
&& RACE_DBC_IDS_BY_CLASS[characterClass.id].includes(raceDbcId)
));
}
export function supportedGenders(raceId: RaceId): readonly GenderId[] {
return raceById(raceId).genders ?? ["male", "female"];
}
export function createDefaultAppearance(): CharacterAppearance {
return { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 };
}
export function clampAppearance(
raceId: RaceId,
gender: GenderId,
appearance: CharacterAppearance,
): CharacterAppearance {
const limits = raceById(raceId).counts[gender];
return Object.fromEntries(APPEARANCE_KEYS.map((key) => [
key,
Math.max(0, Math.min(limits[key] - 1, Math.trunc(appearance[key]))),
])) as unknown as CharacterAppearance;
}
export function randomAppearance(
raceId: RaceId,
gender: GenderId,
random: () => number = Math.random,
): CharacterAppearance {
const limits = raceById(raceId).counts[gender];
return Object.fromEntries(APPEARANCE_KEYS.map((key) => [key, Math.floor(random() * limits[key])])) as unknown as CharacterAppearance;
}
export function normalizeCharacterName(value: string): string {
const letters = value.replace(/[^A-Za-z]/g, "").slice(0, 12).toLowerCase();
return letters ? letters[0].toUpperCase() + letters.slice(1) : "";
}
export function validateCharacterName(value: string): string | null {
if (value.length < 2 || value.length > 12) return "Name must be 2 to 12 letters.";
if (!/^[A-Za-z]+$/.test(value)) return "Use letters only.";
return null;
}
export function totalRaceClassCombinations(): number {
return RACES.reduce((total, race) => total + classesForRace(race.id).length, 0);
}