updating buff and debuff icons. separated COA classes from WoW classes on the character creation screen

This commit is contained in:
phenom
2026-08-16 11:16:51 -04:00
parent b5c9bce1aa
commit c00f3211ea
39 changed files with 2037 additions and 208 deletions
+17
View File
@@ -124,6 +124,23 @@ describe("local profile repository", () => {
});
});
it("migrates custom-class profiles into the CoA category", () => {
const storage = new MemoryStorage();
const offline = createOfflineSession();
const character = createCharacter({
ownerId: offline.ownerId,
name: "Conqueror",
categoryId: "wow",
raceId: "human",
classId: "barbarian",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
expect(character.categoryId).toBe("coa");
expect(listCharacters(offline.ownerId, storage)[0]?.categoryId).toBe("coa");
});
it("persists controller ability bindings per character without changing progression", () => {
const storage = new MemoryStorage();
const offline = createOfflineSession();
+2 -5
View File
@@ -1,6 +1,6 @@
import type { CharacterProfile, PlayerSession } from "./types";
import {
contentCategoryForRace,
contentCategoryForClass,
isRomClassId,
type RomClassId,
} from "./characterCatalog";
@@ -112,7 +112,6 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
equipment?: unknown;
manastormProgress?: unknown;
settings?: unknown;
categoryId?: unknown;
secondaryClassId?: unknown;
actionLoadouts?: unknown;
};
@@ -132,9 +131,7 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
.filter(([, abilityId]) => abilityId === null || typeof abilityId === "string")
.map(([bindingId, abilityId]) => [bindingId, abilityId as string | null]))
: {};
const categoryId = legacy.categoryId === "rom" || legacy.categoryId === "wow"
? legacy.categoryId
: contentCategoryForRace(value.raceId);
const categoryId = contentCategoryForClass(value.classId);
const secondaryClassId = categoryId === "rom"
&& isRomClassId(value.classId)
&& typeof legacy.secondaryClassId === "string"
+27 -12
View File
@@ -8,12 +8,14 @@ import {
classIconUrl,
classesForRace,
clampAppearance,
contentCategoryForClass,
normalizeCharacterName,
randomAppearance,
supportedGenders,
totalRaceClassCombinations,
validateCharacterName,
} from "./characterCatalog";
import { CONTENT_CATEGORIES } from "./contentCategories";
describe("wow335a character catalog", () => {
it("limits normal play to the live-verified race roster", () => {
@@ -34,9 +36,13 @@ describe("wow335a character catalog", () => {
it("matches the extracted Ascension playable catalog", () => {
const wowRaces = RACES.filter((race) => race.categoryId !== "rom");
const wowClasses = CLASSES.filter((characterClass) => characterClass.categoryId !== "rom");
const azerothClasses = CLASSES.filter((characterClass) => characterClass.categoryId !== "rom");
const wowClasses = CLASSES.filter((characterClass) => contentCategoryForClass(characterClass.id) === "wow");
const coaClasses = CLASSES.filter((characterClass) => contentCategoryForClass(characterClass.id) === "coa");
expect(wowRaces).toHaveLength(27);
expect(wowClasses).toHaveLength(31);
expect(azerothClasses).toHaveLength(31);
expect(wowClasses).toHaveLength(10);
expect(coaClasses).toHaveLength(21);
expect(COA_CLASS_IDS).toHaveLength(21);
expect(wowRaces.reduce((total, race) => total + classesForRace(race.id).length, 0)).toBe(740);
expect(totalRaceClassCombinations()).toBe(758);
@@ -48,7 +54,7 @@ describe("wow335a character catalog", () => {
"Forest Troll", "Taunka", "Northrend Skeleton", "Ice Troll", "Earthen",
"Human Cultist",
]);
expect(COA_CLASS_IDS.map((id) => wowClasses.find((entry) => entry.id === id)?.name)).toEqual([
expect(COA_CLASS_IDS.map((id) => coaClasses.find((entry) => entry.id === id)?.name)).toEqual([
"Barbarian", "Witch Doctor", "Felsworn", "Witch Hunter", "Stormbringer",
"Knight of Xoroth", "Guardian", "Templar", "Bloodmage", "Ranger",
"Chronomancer", "Necromancer", "Pyromancer", "Cultist", "Starcaller",
@@ -56,16 +62,25 @@ describe("wow335a character catalog", () => {
]);
});
it("separates default WoW, custom CoA, and RoM character creation categories", () => {
expect(CONTENT_CATEGORIES.map((category) => category.id)).toEqual(["wow", "rom", "coa"]);
expect(classesForRace("human", "wow").map((entry) => entry.id)).toContain("priest");
expect(classesForRace("human", "wow").map((entry) => entry.id)).not.toContain("barbarian");
expect(classesForRace("human", "coa").map((entry) => entry.id)).toContain("barbarian");
expect(classesForRace("human", "coa").map((entry) => entry.id)).not.toContain("priest");
expect(classesForRace("rom-human", "rom").map((entry) => entry.id)).toContain("rom-priest");
});
it("applies the race eligibility rows from CharBaseInfo.dbc", () => {
const wowClassCount = CLASSES.filter((characterClass) => characterClass.categoryId !== "rom").length;
expect(classesForRace("human").map((entry) => entry.id)).toContain("barbarian");
expect(classesForRace("human").map((entry) => entry.id)).not.toContain("felsworn");
expect(classesForRace("draenei").map((entry) => entry.id)).toContain("felsworn");
expect(classesForRace("draenei").map((entry) => entry.id)).toContain("knight-of-xoroth");
expect(classesForRace("troll").map((entry) => entry.id)).toContain("venomancer");
expect(classesForRace("troll").map((entry) => entry.id)).not.toContain("knight-of-xoroth");
expect(classesForRace("goblin")).toHaveLength(wowClassCount);
expect(classesForRace("human-cultist")).toHaveLength(wowClassCount);
expect(classesForRace("human", "coa").map((entry) => entry.id)).toContain("barbarian");
expect(classesForRace("human", "coa").map((entry) => entry.id)).not.toContain("felsworn");
expect(classesForRace("draenei", "coa").map((entry) => entry.id)).toContain("felsworn");
expect(classesForRace("draenei", "coa").map((entry) => entry.id)).toContain("knight-of-xoroth");
expect(classesForRace("troll", "coa").map((entry) => entry.id)).toContain("venomancer");
expect(classesForRace("troll", "coa").map((entry) => entry.id)).not.toContain("knight-of-xoroth");
expect(classesForRace("goblin", "wow")).toHaveLength(10);
expect(classesForRace("goblin", "coa")).toHaveLength(21);
expect(classesForRace("human-cultist")).toHaveLength(31);
expect(supportedGenders("vrykul")).toEqual(["male"]);
expect(supportedGenders("earthen")).toEqual(["male", "female"]);
});
+41 -25
View File
@@ -408,27 +408,27 @@ export const CLASSES: readonly ClassDefinition[] = [
{ 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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: "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" },
@@ -595,21 +595,37 @@ export function isRomClassId(classId: ClassId): classId is RomClassId {
}
export function racesForCategory(categoryId: ContentCategoryId): readonly RaceDefinition[] {
return PLAYABLE_RACES.filter((race) => (race.categoryId ?? "wow") === categoryId);
return PLAYABLE_RACES.filter((race) => {
const raceCategory = race.categoryId ?? "wow";
return raceCategory === categoryId || (categoryId === "coa" && raceCategory === "wow");
});
}
export function classesForRace(raceId: RaceId): readonly ClassDefinition[] {
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) => (
(characterClass.categoryId ?? "wow") === "wow"
classMatchesCategory(characterClass)
));
const raceDbcId = race.dbcId;
return CLASSES.filter((characterClass) => (
(characterClass.categoryId ?? "wow") === "wow"
classMatchesCategory(characterClass)
&& RACE_DBC_IDS_BY_CLASS[characterClass.id].includes(raceDbcId)
));
}
+6 -2
View File
@@ -1,4 +1,4 @@
export type ContentCategoryId = "wow" | "rom";
export type ContentCategoryId = "wow" | "rom" | "coa";
export interface ContentCategoryDefinition {
readonly id: ContentCategoryId;
@@ -6,9 +6,13 @@ export interface ContentCategoryDefinition {
readonly name: string;
}
/** Shared by character creation and dungeon selection. */
/** Character-creation categories; other catalogs may expose a supported subset. */
export const CONTENT_CATEGORIES: readonly ContentCategoryDefinition[] = Object.freeze([
{ id: "wow", label: "WoW", name: "World of Warcraft" },
{ id: "rom", label: "RoM", name: "Runes of Magic" },
{ id: "coa", label: "CoA", name: "Conquest of Azeroth" },
]);
export function contentCategoryById(id: ContentCategoryId | null | undefined): ContentCategoryDefinition {
return CONTENT_CATEGORIES.find((category) => category.id === id) ?? CONTENT_CATEGORIES[0];
}
+11 -1
View File
@@ -163,7 +163,7 @@ describe("shell store", () => {
expect(useShellStore.getState().draft.gender).toBe("male");
});
it("resets the creator to a valid draft when switching between WoW and RoM", () => {
it("resets the creator to a valid draft when switching among WoW, RoM, and CoA", () => {
useShellStore.getState().openCreator();
useShellStore.getState().updateDraft({ name: "Category Keeper", categoryId: "rom" });
expect(useShellStore.getState().draft).toMatchObject({
@@ -179,6 +179,16 @@ describe("shell store", () => {
classId: "rom-champion",
gender: "male",
});
useShellStore.getState().updateDraft({ categoryId: "coa" });
expect(useShellStore.getState().draft).toMatchObject({
name: "Categorykeep",
categoryId: "coa",
raceId: "human",
classId: "barbarian",
gender: "female",
});
useShellStore.getState().updateDraft({ classId: "priest" });
expect(useShellStore.getState().draft.classId).toBe("barbarian");
useShellStore.getState().updateDraft({ categoryId: "wow" });
expect(useShellStore.getState().draft).toMatchObject({
name: "Categorykeep",
+6 -6
View File
@@ -19,10 +19,10 @@ import {
clampAppearance,
classById,
classesForRace,
contentCategoryForRace,
createDefaultAppearance,
normalizeCharacterName,
randomAppearance,
raceSupportsCategory,
racesForCategory,
supportedGenders,
validateCharacterName,
@@ -100,7 +100,7 @@ function defaultDraft(categoryId: ContentCategoryId = "wow"): CharacterDraft {
name: "",
categoryId,
raceId,
classId: categoryId === "rom" ? "rom-priest" : "priest",
classId: categoryId === "rom" ? "rom-priest" : categoryId === "coa" ? "barbarian" : "priest",
gender: "female",
appearance: createDefaultAppearance(),
rotation: -12,
@@ -165,16 +165,16 @@ export const useShellStore = create<ShellState>((set, get) => ({
? { ...defaultDraft(patch.categoryId), name: state.draft.name, rotation: state.draft.rotation }
: { ...state.draft, ...patch };
if (patch.name !== undefined) next.name = normalizeCharacterName(patch.name);
if (contentCategoryForRace(next.raceId) !== next.categoryId) {
if (!raceSupportsCategory(next.raceId, next.categoryId)) {
const targetRace = racesForCategory(next.categoryId)[0];
next.raceId = targetRace.id;
next.classId = classesForRace(targetRace.id)[0].id;
next.classId = classesForRace(targetRace.id, next.categoryId)[0].id;
next.gender = supportedGenders(targetRace.id)[0] ?? "male";
next.appearance = createDefaultAppearance();
}
const availableGenders = supportedGenders(next.raceId);
if (!availableGenders.includes(next.gender)) next.gender = availableGenders[0] ?? "male";
const availableClasses = classesForRace(next.raceId);
const availableClasses = classesForRace(next.raceId, next.categoryId);
if (!availableClasses.some((definition) => definition.id === next.classId)) {
next.classId = availableClasses[0].id;
}
@@ -201,7 +201,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
set({ notice: nameError });
return false;
}
if (!classesForRace(draft.raceId).some((definition) => definition.id === draft.classId)) {
if (!classesForRace(draft.raceId, draft.categoryId).some((definition) => definition.id === draft.classId)) {
set({ notice: "That class is not available to the selected race." });
return false;
}