forked from phenom/healer-man
updating buff and debuff icons. separated COA classes from WoW classes on the character creation screen
This commit is contained in:
1 parent
b5c9bce1aa
commit
c00f3211ea
39 files changed
+2037
-208
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user