forked from phenom/healer-man
232 lines
8.9 KiB
TypeScript
232 lines
8.9 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { classById, FACTIONS, raceById } from "../app/characterCatalog";
|
|
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
|
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 { ContentManagerScreen } from "./ContentManagerScreen";
|
|
import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
|
|
import { isGameMasterSession } from "../game/manastormAdminConfig";
|
|
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
|
|
|
const GAME_MODES = [
|
|
{
|
|
id: "dungeons",
|
|
number: "01",
|
|
name: "Dungeons",
|
|
description: "Choose an expedition and lead your party into the depths.",
|
|
available: true,
|
|
},
|
|
{
|
|
id: "manastorms",
|
|
number: "02",
|
|
name: "Manastorms",
|
|
description: "Chain random boss encounters as the storm grows stronger and more rewarding.",
|
|
available: true,
|
|
},
|
|
{
|
|
id: "roguelike-rpg",
|
|
number: "03",
|
|
name: "Roguelike RPG",
|
|
description: "A new mode is taking shape.",
|
|
available: false,
|
|
},
|
|
{
|
|
id: "trials",
|
|
number: "04",
|
|
name: "Trials",
|
|
description: "A new mode is taking shape.",
|
|
available: false,
|
|
},
|
|
{
|
|
id: "downloads",
|
|
number: "05",
|
|
name: "Downloads",
|
|
description: "Install, update, or remove adventures that remain playable offline.",
|
|
available: true,
|
|
},
|
|
{
|
|
id: "options",
|
|
number: "06",
|
|
name: "Options",
|
|
description: "Main-menu settings are coming soon.",
|
|
available: false,
|
|
},
|
|
{
|
|
id: "player-profile",
|
|
number: "07",
|
|
name: "Player Profile",
|
|
description: "Profile details are coming soon.",
|
|
available: false,
|
|
},
|
|
] as const;
|
|
|
|
export function MainMenuScreen() {
|
|
const [contentManagerOpen, setContentManagerOpen] = useState(false);
|
|
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
|
const rosterCharacter = useShellStore(selectedCharacter);
|
|
const openDungeons = useShellStore((state) => state.openDungeons);
|
|
const openManastorms = useShellStore((state) => state.openManastorms);
|
|
const openClassHall = useShellStore((state) => state.openClassHall);
|
|
const returnToCharacters = useShellStore((state) => state.returnToCharacters);
|
|
const notice = useShellStore((state) => state.notice);
|
|
const session = useShellStore((state) => state.session);
|
|
const openGmAdmin = useShellStore((state) => state.openGmAdmin);
|
|
const character = activeCharacter ?? rosterCharacter;
|
|
const isRom = character?.categoryId === "rom";
|
|
const isGm = isGameMasterSession(session);
|
|
|
|
const actions = useMemo<MenuAction[]>(() => [
|
|
{
|
|
id: "mode-dungeons",
|
|
run: openDungeons,
|
|
neighbors: { right: "mode-manastorms", down: "back-to-roster" },
|
|
},
|
|
{
|
|
id: "mode-manastorms",
|
|
run: openManastorms,
|
|
neighbors: { left: "mode-dungeons", down: "back-to-roster" },
|
|
},
|
|
{
|
|
id: "mode-downloads",
|
|
run: () => setContentManagerOpen(true),
|
|
neighbors: { left: "mode-manastorms", down: "back-to-roster" },
|
|
},
|
|
{
|
|
id: "back-to-roster",
|
|
run: returnToCharacters,
|
|
neighbors: { up: "mode-dungeons", left: "mode-dungeons" },
|
|
},
|
|
...(isRom ? [{ id: "mode-class-hall", run: openClassHall }] : []),
|
|
...(isGm ? [{ id: "mode-gm-tools", run: openGmAdmin }] : []),
|
|
], [isGm, isRom, openClassHall, openDungeons, openGmAdmin, openManastorms, returnToCharacters]);
|
|
const controller = useMenuController(actions, {
|
|
initialId: "mode-dungeons",
|
|
onBack: returnToCharacters,
|
|
});
|
|
|
|
const race = character ? raceById(character.raceId) : null;
|
|
const characterClass = character ? classById(character.classId) : null;
|
|
|
|
if (contentManagerOpen) return <ContentManagerScreen onClose={() => setContentManagerOpen(false)} />;
|
|
|
|
return (
|
|
<DualDisplayFrame
|
|
contextLabel="Main menu details"
|
|
top={
|
|
<FrontSurface className="main-menu-screen" ariaLabel="Healer Man main menu">
|
|
<header className="main-menu-header">
|
|
<BrandMark compact />
|
|
<div>
|
|
<span>Adventurer selected</span>
|
|
<strong>Choose your next journey</strong>
|
|
</div>
|
|
<ControllerButton
|
|
controlId="back-to-roster"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
type="button"
|
|
onClick={returnToCharacters}
|
|
>
|
|
Change character
|
|
</ControllerButton>
|
|
</header>
|
|
|
|
<aside className="main-menu-character" aria-label="Selected character">
|
|
<div className="main-menu-character__preview">
|
|
<CharacterPreview character={character} />
|
|
</div>
|
|
{character && race && characterClass && (
|
|
<div className="main-menu-character__summary">
|
|
<p className="front-kicker">Ready for adventure</p>
|
|
<h1>{character.name}</h1>
|
|
<strong style={{ color: characterClass.color }}>
|
|
Level {character.level} {race.name} {characterClass.name}
|
|
</strong>
|
|
<small>{FACTIONS[race.faction].name} / {character.gender}</small>
|
|
{isRom && <small>RoM / {character.secondaryClassId ? `${characterClass.name} + ${classById(character.secondaryClassId).name}` : "Secondary class not chosen"}</small>}
|
|
</div>
|
|
)}
|
|
</aside>
|
|
|
|
<main className="main-menu-modes">
|
|
<header>
|
|
<p className="front-kicker">Main menu</p>
|
|
<h2>Ways to play</h2>
|
|
<p>Select a destination. More adventures will unlock here as they are built.</p>
|
|
</header>
|
|
<OnlineGroupPanel />
|
|
<nav className="main-menu-mode-grid" aria-label="Game modes">
|
|
{isGm && (
|
|
<ControllerButton
|
|
controlId="mode-gm-tools"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available main-menu-mode--gm"
|
|
type="button"
|
|
onClick={openGmAdmin}
|
|
>
|
|
<b aria-hidden="true">GM</b>
|
|
<span><strong>GM Tools</strong><small>Manage Manastorm rules, boss readiness, and encounter placement.</small></span>
|
|
<em>Authorized</em>
|
|
</ControllerButton>
|
|
)}
|
|
{isRom && (
|
|
<ControllerButton
|
|
controlId="mode-class-hall"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available main-menu-mode--class-hall"
|
|
type="button"
|
|
onClick={openClassHall}
|
|
>
|
|
<b aria-hidden="true">CH</b>
|
|
<span><strong>Class Hall</strong><small>Choose a permanent partner class and swap your active ordering.</small></span>
|
|
<em>{character && character.level >= 10 ? "Open" : "Unlocks at 10"}</em>
|
|
</ControllerButton>
|
|
)}
|
|
{GAME_MODES.map((mode) => mode.available ? (
|
|
<ControllerButton
|
|
key={mode.id}
|
|
controlId={`mode-${mode.id}`}
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available"
|
|
type="button"
|
|
onClick={mode.id === "manastorms"
|
|
? openManastorms
|
|
: mode.id === "downloads"
|
|
? () => setContentManagerOpen(true)
|
|
: openDungeons}
|
|
>
|
|
<b aria-hidden="true">{mode.number}</b>
|
|
<span><strong>{mode.name}</strong><small>{mode.description}</small></span>
|
|
<em>Available</em>
|
|
</ControllerButton>
|
|
) : (
|
|
<button
|
|
key={mode.id}
|
|
className="front-button main-menu-mode main-menu-mode--coming-soon"
|
|
type="button"
|
|
disabled
|
|
aria-label={`${mode.name}, coming soon`}
|
|
>
|
|
<b aria-hidden="true">{mode.number}</b>
|
|
<span><strong>{mode.name}</strong><small>{mode.description}</small></span>
|
|
<em>Coming soon</em>
|
|
</button>
|
|
))}
|
|
</nav>
|
|
{notice && <p className="front-notice main-menu-notice" role="status" aria-live="polite">{notice}</p>}
|
|
</main>
|
|
|
|
<ControllerLegend back />
|
|
</FrontSurface>
|
|
}
|
|
bottom={<LazyLocalCompanionPanel />}
|
|
/>
|
|
);
|
|
}
|