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; counts: Record; } 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 { 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 = 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> = { 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> = { 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> = { "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> = { 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); }