import { useMemo, useState } from "react"; import { classById, raceById, type RomClassId } from "../app/characterCatalog"; import { selectedCharacter, useShellStore } from "../app/shellStore"; import type { CharacterProfile } from "../app/types"; import { ROM_ELITE_SKILL_LEVELS, ROM_SECONDARY_UNLOCK_LEVEL, romSecondaryClassesForRace, unlockedRomEliteLevels, } from "../app/romDualClass"; import { DualDisplayFrame } from "../components/DualDisplayFrame"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { CharacterPreview } from "./CharacterPreview"; import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel"; import { ControllerButton } from "./ControllerButton"; import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface"; import { PARTY_ROLE_LABELS } from "../game/partyRoles"; import { specializationById, specializationsForClass, } from "../game/specializationCatalog"; export function ClassHallScreen() { const character = useShellStore(selectedCharacter); if (!character) return null; return character.categoryId === "rom" ? : ; } function SpecializationClassHall({ character }: { readonly character: CharacterProfile }) { const notice = useShellStore((state) => state.notice); const returnToMainMenu = useShellStore((state) => state.returnToMainMenu); const returnToCharacters = useShellStore((state) => state.returnToCharacters); const changeSpecialization = useShellStore((state) => state.changeSelectedSpecialization); const [pendingSpecializationId, setPendingSpecializationId] = useState(null); const options = useMemo(() => specializationsForClass(character.classId), [character.classId]); const current = specializationById(character.specializationId); const forced = current === null; const goBack = forced ? returnToCharacters : returnToMainMenu; const apply = (resetTalents: boolean) => { if (!pendingSpecializationId || !changeSpecialization(pendingSpecializationId, resetTalents)) return; setPendingSpecializationId(null); if (forced) returnToMainMenu(); }; const actions = useMemo(() => pendingSpecializationId ? [ { id: "specialization-cancel", run: () => setPendingSpecializationId(null) }, { id: "specialization-keep", run: () => apply(false) }, { id: "specialization-reset", run: () => apply(true) }, ] : [ ...options.map((definition) => ({ id: `specialization-${definition.id}`, run: () => setPendingSpecializationId(definition.id) })), { id: "class-hall-back", run: goBack }, ], [goBack, options, pendingSpecializationId]); const controller = useMenuController(actions, { initialId: current ? `specialization-${current.id}` : options[0] ? `specialization-${options[0].id}` : "class-hall-back", onBack: pendingSpecializationId ? () => setPendingSpecializationId(null) : goBack, }); const race = raceById(character.raceId); const characterClass = classById(character.classId); const pending = specializationById(pendingSpecializationId); return (
{characterClass.name} / Class Hall{forced ? "Choose your specialization to continue." : "Change your combat path."}
{forced ? "Roster" : "Back"}

Specialization and heirloom profile

{forced ? "A specialization is required" : `Current: ${current.displayName}`}

Your saved specialization locks your activity role and tunes every heirloom stat and weapon slot. Talent points do not change specialization automatically.

{options.map((definition) => ( setPendingSpecializationId(definition.id)}> {characterClass.sigil} {definition.displayName}{PARTY_ROLE_LABELS[definition.role]} / {definition.gearArchetype.replaceAll("-", " ")} / {definition.weaponLayout.replaceAll("-", " ")} ))}
{notice &&

{notice}

}
{pending && (

Specialization change

Become {pending.displayName}?

Heirloom stats and your allowed activity role change immediately. Choose whether to preserve the current talent build and action bindings.

setPendingSpecializationId(null)}>Cancel apply(false)}>Keep talents apply(true)}>Reset talents
)} } bottom={} /> ); } function RomClassHall({ character }: { readonly character: CharacterProfile }) { const notice = useShellStore((state) => state.notice); const returnToMainMenu = useShellStore((state) => state.returnToMainMenu); const chooseSecondary = useShellStore((state) => state.chooseRomSecondaryClass); const swapClasses = useShellStore((state) => state.swapSelectedRomClasses); const [pendingClassId, setPendingClassId] = useState(null); const options = useMemo( () => character ? romSecondaryClassesForRace(character.raceId, character.classId) : [], [character], ); const actions = useMemo(() => pendingClassId ? [ { id: "secondary-cancel", run: () => setPendingClassId(null) }, { id: "secondary-confirm", run: () => { if (chooseSecondary(pendingClassId)) setPendingClassId(null); } }, ] : [ ...options.map((classId) => ({ id: `secondary-${classId}`, run: () => setPendingClassId(classId) })), ...(character?.secondaryClassId ? [{ id: "swap-classes", run: swapClasses }] : []), { id: "class-hall-back", run: returnToMainMenu }, ], [character?.secondaryClassId, chooseSecondary, options, pendingClassId, returnToMainMenu, swapClasses]); const controller = useMenuController(actions, { initialId: character?.secondaryClassId ? "swap-classes" : options[0] ? `secondary-${options[0]}` : "class-hall-back", onBack: pendingClassId ? () => setPendingClassId(null) : returnToMainMenu, }); const race = raceById(character.raceId); const primary = classById(character.classId); const secondary = character.secondaryClassId ? classById(character.secondaryClassId) : null; const locked = character.level < ROM_SECONDARY_UNLOCK_LEVEL; const eliteLevels = unlockedRomEliteLevels(character.level); return (
Runes of Magic / Class HallBind two paths. Master both directions.
Back
{locked ? (
10

The second path opens at level 10

Your level and experience are shared. Return after reaching level {ROM_SECONDARY_UNLOCK_LEVEL}.

) : secondary ? ( <>

Active ordered pair

{primary.name} / {secondary.name}

The primary contributes full base stats and determines your role and resource. {secondary.name} contributes 10% of its level-scaled base stats and its general skills.

Swap to {secondary.name} / {primary.name}

Automatic elite skills

{eliteLevels.length} / 10 unlocked

{ROM_ELITE_SKILL_LEVELS.map((level) => Lv {level})}
Only the active ordering's elite set appears in combat. Swapping restores that orientation's action bar.
) : (

Permanent secondary class

Choose {character.name}'s second path

This choice cannot be replaced. You can swap which of the pair is primary whenever you return here outside a run.

{options.map((classId) => { const definition = classById(classId); return setPendingClassId(classId)}>{definition.sigil}{definition.name}{definition.role} / {definition.resourceSummary}; })}
)} {notice &&

{notice}

}
{pendingClassId && (

Irreversible choice

Bind {classById(pendingClassId).name} to {primary.name}?

The pair is permanent. Its direction may be swapped here later, but the partner class cannot be replaced.

setPendingClassId(null)}>Go back { if (chooseSecondary(pendingClassId)) setPendingClassId(null); }}>Confirm permanent pair
)} } bottom={} /> ); }