import { useMemo, useRef } from "react"; import { APPEARANCE_KEYS, FACTIONS, classById, classIconUrl, classesForRace, createDefaultAppearance, raceById, racesForCategory, supportedGenders, type AppearanceKey, type GenderId, type RaceId, } from "../app/characterCatalog"; import { CONTENT_CATEGORIES, type ContentCategoryId } from "../app/contentCategories"; import { useShellStore } from "../app/shellStore"; import { DualDisplayFrame } from "../components/DualDisplayFrame"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { CharacterPreview } from "./CharacterPreview"; import { ControllerButton } from "./ControllerButton"; import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface"; import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel"; import { PARTY_ROLE_LABELS } from "../game/partyRoles"; import { specializationsForClass } from "../game/specializationCatalog"; export function CharacterCreateScreen() { const draft = useShellStore((state) => state.draft); const notice = useShellStore((state) => state.notice); const updateDraft = useShellStore((state) => state.updateDraft); const setStep = useShellStore((state) => state.setCreatorStep); const randomize = useShellStore((state) => state.randomizeDraft); const createCharacter = useShellStore((state) => state.createDraftCharacter); const cancel = useShellStore((state) => state.cancelCreator); const nameRef = useRef(null); const race = raceById(draft.raceId); const characterClass = classById(draft.classId); const availableRaces = useMemo(() => racesForCategory(draft.categoryId), [draft.categoryId]); const availableClasses = useMemo( () => classesForRace(draft.raceId, draft.categoryId), [draft.categoryId, draft.raceId], ); const appearanceLabels = race.labels[draft.gender]; const appearanceCounts = race.counts[draft.gender]; const specializationOptions = useMemo(() => specializationsForClass(draft.classId), [draft.classId]); const chooseRace = (raceId: RaceId) => { const nextClasses = classesForRace(raceId, draft.categoryId); const nextGenders = supportedGenders(raceId); const classId = nextClasses.some((definition) => definition.id === draft.classId) ? draft.classId : nextClasses[0].id; const gender = nextGenders.includes(draft.gender) ? draft.gender : nextGenders[0]; updateDraft({ raceId, classId, gender, appearance: createDefaultAppearance() }); }; const chooseCategory = (categoryId: ContentCategoryId) => updateDraft({ categoryId }); const chooseGender = (gender: GenderId) => { if (!supportedGenders(draft.raceId).includes(gender)) return; updateDraft({ gender, appearance: createDefaultAppearance() }); }; const adjustAppearance = (key: AppearanceKey, delta: number) => { const count = appearanceCounts[key]; const value = (draft.appearance[key] + delta + count) % count; updateDraft({ appearance: { ...draft.appearance, [key]: value } }); }; const goBack = () => draft.step === "appearance" ? setStep("identity") : cancel(); const actions = useMemo(() => draft.step === "identity" ? [ ...CONTENT_CATEGORIES.map((definition) => ({ id: `category-${definition.id}`, run: () => chooseCategory(definition.id) })), ...availableRaces.map((definition) => ({ id: `race-${definition.id}`, run: () => chooseRace(definition.id) })), ...supportedGenders(draft.raceId).map((gender) => ({ id: `gender-${gender}`, run: () => chooseGender(gender), })), ...availableClasses.map((definition) => ({ id: `class-${definition.id}`, run: () => updateDraft({ classId: definition.id }) })), ...specializationOptions.map((definition) => ({ id: `spec-${definition.id}`, run: () => updateDraft({ specializationId: definition.id }) })), { id: "rotate-left", run: () => updateDraft({ rotation: draft.rotation - 12 }) }, { id: "rotate-right", run: () => updateDraft({ rotation: draft.rotation + 12 }) }, { id: "creator-back", run: cancel }, { id: "creator-next", run: () => setStep("appearance") }, ] : [ { id: "character-name", run: () => nameRef.current?.focus() }, ...APPEARANCE_KEYS.flatMap((key) => [ { id: `${key}-minus`, run: () => adjustAppearance(key, -1) }, { id: `${key}-plus`, run: () => adjustAppearance(key, 1) }, ]), { id: "randomize", run: randomize }, { id: "appearance-back", run: () => setStep("identity") }, { id: "create-character", run: createCharacter }, ], [draft, appearanceCounts, availableClasses, availableRaces, cancel, createCharacter, randomize, setStep, specializationOptions, updateDraft]); const controller = useMenuController(actions, { columns: draft.step === "identity" ? 5 : 2, initialId: draft.step === "identity" ? `race-${draft.raceId}` : "character-name", onBack: goBack, }); return (
Character forge / Step {draft.step === "identity" ? "1" : "2"} of 2{draft.step === "identity" ? "Choose your path" : "Make them your own"}
{draft.step === "identity" && }
{draft.step === "identity" ? ( <>
01
Race{FACTIONS[race.faction].name}
{availableRaces.map((definition) => ( chooseRace(definition.id)} title={definition.description} > {definition.sigil}{definition.name}{definition.faction === "taborea" ? "R" : definition.faction === "alliance" ? "A" : "H"} ))}
{(["male", "female"] as const).map((gender) => { const available = supportedGenders(draft.raceId).includes(gender); return ( chooseGender(gender)} > {gender} ); })}

{race.description}

updateDraft({ rotation: draft.rotation - 12 })} aria-label="Rotate character left">↺ Rotate preview updateDraft({ rotation: draft.rotation + 12 })} aria-label="Rotate character right">↻
02
Class{availableClasses.length} {draft.categoryId === "rom" ? "RuneWaker" : draft.categoryId === "coa" ? "CoA" : "WoW"} classes available to {race.name}
{availableClasses.map((definition) => ( updateDraft({ classId: definition.id })} title={`${definition.description} ${definition.specializations.join(" / ")}`.trim()} > {definition.name}{definition.role} {definition.mode === "conquest" && CoA} ))}
{characterClass.name} {characterClass.description} {characterClass.armor} / {characterClass.resourceSummary} / {characterClass.specializations.length ? characterClass.specializations.join(" ยท ") : "Automatic skill ranks"}
{specializationOptions.length > 0 && (
Specialization {specializationOptions.map((definition) => ( updateDraft({ specializationId: definition.id })} > {definition.displayName} {PARTY_ROLE_LABELS[definition.role]} / {definition.talentTree} ))}
)}
Back to roster setStep("appearance")}>Customize & nameNext step
) : ( <>

{race.name} {characterClass.name}{draft.gender} / {FACTIONS[race.faction].name}

{ event.preventDefault(); createCharacter(); }}>

Identity

Name your adventurer

Names follow the wow335a 2-12 letter limit.
updateDraft({ name: event.target.value })} onFocus={() => controller.select("character-name")} minLength={2} maxLength={12} pattern="[A-Za-z]+" placeholder="Remedy" autoComplete="off" />
{APPEARANCE_KEYS.map((key) => (
{appearanceLabels[key]}{draft.appearance[key] + 1} / {appearanceCounts[key]} adjustAppearance(key, -1)} aria-label={`Previous ${appearanceLabels[key]}`}>- updateDraft({ appearance: { ...draft.appearance, [key]: Number(event.target.value) } })} aria-label={appearanceLabels[key]} /> adjustAppearance(key, 1)} aria-label={`Next ${appearanceLabels[key]}`}>+
))}
Randomize appearance setStep("identity")}>Back Create characterSave to this roster
{notice &&

{notice}

}
)} } bottom={} /> ); }