454 lines
28 KiB
TypeScript
454 lines
28 KiB
TypeScript
import { lazy, Suspense, useCallback, useMemo, useState } from "react";
|
|
import { classById } from "../app/characterCatalog";
|
|
import { queueRandomDungeon, type RandomDungeonQueueMode, type RandomDungeonQueueStage } from "../app/randomDungeonQueue";
|
|
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
|
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
|
import { useContentManagerSnapshot } from "../content/useContentManager";
|
|
import { DualDisplayFrame } from "../components/DualDisplayFrame";
|
|
import { useCompanionPartyPreferences } from "../game/companionPartyPreferences";
|
|
import { partyRolesForClass, PARTY_ROLE_LABELS, type PartyRole } from "../game/partyStore";
|
|
import { specializationById, specializationIconUrl } from "../game/specializationCatalog";
|
|
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
|
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
|
|
import { ControllerButton } from "./ControllerButton";
|
|
import { ContentManagerScreen } from "./ContentManagerScreen";
|
|
import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
|
|
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
|
import { sessionCanUseGmTools, SHELVED_MODES_ENABLED } from "../app/releaseFeatures";
|
|
import { DungeonReportsScreen } from "./DungeonReportsScreen";
|
|
import { MainMenuCharacterEditor, type MainMenuEditor } from "./MainMenuCharacterEditor";
|
|
const BossCardCollectionScreen = lazy(() => import("./BossCardCollection"));
|
|
const BossTestingScreen = lazy(() => import("./BossTestingScreen").then((module) => ({ default: module.BossTestingScreen })));
|
|
const BalanceLabScreen = lazy(() => import("./BalanceLabScreen").then((module) => ({ default: module.BalanceLabScreen })));
|
|
const BalanceTuningScreen = lazy(() => import("./BalanceTuningScreen").then((module) => ({ default: module.BalanceTuningScreen })));
|
|
const CompanionReadinessScreen = lazy(() => import("./CompanionReadinessScreen").then((module) => ({ default: module.CompanionReadinessScreen })));
|
|
const RandomizedBiteSizedScreen = lazy(() => import("./RandomizedBiteSizedScreen").then((module) => ({ default: module.RandomizedBiteSizedScreen })));
|
|
|
|
const GAME_MODES = [
|
|
{
|
|
id: "dungeons",
|
|
number: "01",
|
|
name: "Dungeons",
|
|
description: "Choose an expedition and lead your party into the depths.",
|
|
available: true,
|
|
},
|
|
{
|
|
id: "random-dungeon",
|
|
number: "02",
|
|
name: "Random Dungeon",
|
|
description: "Let fate choose an available full dungeon from WoW or RoM.",
|
|
available: true,
|
|
},
|
|
{
|
|
id: "random-bite-sized",
|
|
number: "03",
|
|
name: "Random Regular Bite-Sized Dungeon",
|
|
description: "Choose a short authored dungeon with its regular enemies and boss rules.",
|
|
available: true,
|
|
},
|
|
{
|
|
id: "randomized-bite-sized",
|
|
number: "04",
|
|
name: "Randomized Bite-Sized",
|
|
description: "Run a short dungeon with every mob and the final boss rerolled.",
|
|
available: true,
|
|
},
|
|
...(SHELVED_MODES_ENABLED ? [{
|
|
id: "manastorms" as const,
|
|
number: "02",
|
|
name: "Manastorms",
|
|
description: "Chain random boss encounters as the storm grows stronger and more rewarding.",
|
|
available: true,
|
|
}, {
|
|
id: "arenas" as const,
|
|
number: "03",
|
|
name: "Arenas",
|
|
description: "Rated 2v2 and 3v3 combat against players or rating-matched opponents.",
|
|
available: true,
|
|
}] : []),
|
|
{
|
|
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 [reportsOpen, setReportsOpen] = useState(false);
|
|
const [cardsOpen, setCardsOpen] = useState(false);
|
|
const [bossTestingOpen, setBossTestingOpen] = useState(false);
|
|
const [balanceLabOpen, setBalanceLabOpen] = useState(false);
|
|
const [balanceTuningOpen, setBalanceTuningOpen] = useState(false);
|
|
const [readinessOpen, setReadinessOpen] = useState(false);
|
|
const [randomizedBiteSizedOpen, setRandomizedBiteSizedOpen] = useState(false);
|
|
const [editor, setEditor] = useState<MainMenuEditor | null>(null);
|
|
if (editor) return <MainMenuCharacterEditor editor={editor} onClose={() => setEditor(null)} />;
|
|
if (bossTestingOpen) return <Suspense fallback={<p role="status">Opening Boss Testing…</p>}><BossTestingScreen onClose={() => setBossTestingOpen(false)} /></Suspense>;
|
|
if (balanceLabOpen) return <Suspense fallback={<p role="status">Opening Balance Lab…</p>}><BalanceLabScreen onClose={() => setBalanceLabOpen(false)} /></Suspense>;
|
|
if (balanceTuningOpen) return <Suspense fallback={<p role="status">Opening balance tuning…</p>}><BalanceTuningScreen onClose={() => setBalanceTuningOpen(false)} /></Suspense>;
|
|
if (readinessOpen) return <Suspense fallback={<p role="status">Opening Companion Readiness…</p>}><CompanionReadinessScreen onClose={() => setReadinessOpen(false)} /></Suspense>;
|
|
if (randomizedBiteSizedOpen) return <Suspense fallback={<p role="status">Opening Randomized Bite-Sized…</p>}><RandomizedBiteSizedScreen onClose={() => setRandomizedBiteSizedOpen(false)} /></Suspense>;
|
|
if (cardsOpen) return <Suspense fallback={<p role="status">Opening collection log…</p>}><BossCardCollectionScreen onClose={() => setCardsOpen(false)} /></Suspense>;
|
|
if (contentManagerOpen) return <ContentManagerScreen onClose={() => setContentManagerOpen(false)} />;
|
|
if (reportsOpen) return <DungeonReportsScreen onClose={() => setReportsOpen(false)} />;
|
|
return <MainMenuDestinations onOpenDownloads={() => setContentManagerOpen(true)} onOpenReports={() => setReportsOpen(true)} onOpenCards={() => setCardsOpen(true)} onOpenBossTesting={() => setBossTestingOpen(true)} onOpenBalanceLab={() => setBalanceLabOpen(true)} onOpenBalanceTuning={() => setBalanceTuningOpen(true)} onOpenReadiness={() => setReadinessOpen(true)} onOpenRandomizedBiteSized={() => setRandomizedBiteSizedOpen(true)} onOpenEditor={setEditor} />;
|
|
}
|
|
|
|
function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onOpenBossTesting, onOpenBalanceLab, onOpenBalanceTuning, onOpenReadiness, onOpenRandomizedBiteSized, onOpenEditor }: {
|
|
readonly onOpenDownloads: () => void;
|
|
readonly onOpenReports: () => void;
|
|
readonly onOpenCards: () => void;
|
|
readonly onOpenBossTesting: () => void;
|
|
readonly onOpenBalanceLab: () => void;
|
|
readonly onOpenBalanceTuning: () => void;
|
|
readonly onOpenReadiness: () => void;
|
|
readonly onOpenRandomizedBiteSized: () => void;
|
|
readonly onOpenEditor: (editor: MainMenuEditor) => void;
|
|
}) {
|
|
const [gmMenuOpen, setGmMenuOpen] = useState(false);
|
|
const [randomQueueMode, setRandomQueueMode] = useState<RandomDungeonQueueMode | null>(null);
|
|
const [selectedRole, setSelectedRole] = useState<PartyRole | null>(null);
|
|
const [queueing, setQueueing] = useState(false);
|
|
const [queueStage, setQueueStage] = useState<RandomDungeonQueueStage>("rolling");
|
|
const [queuedDungeonTitle, setQueuedDungeonTitle] = useState("");
|
|
const [queueError, setQueueError] = useState("");
|
|
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
|
const rosterCharacter = useShellStore(selectedCharacter);
|
|
const openDungeons = useShellStore((state) => state.openDungeons);
|
|
const openRandomDungeon = useCallback(() => { setRandomQueueMode("regular"); setSelectedRole(null); setQueueError(""); }, []);
|
|
const openRandomBiteSizedDungeon = useCallback(() => { setRandomQueueMode("bite-sized"); setSelectedRole(null); setQueueError(""); }, []);
|
|
const openManastorms = useShellStore((state) => state.openManastorms);
|
|
const openArenas = useShellStore((state) => state.openArenas);
|
|
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 enterDungeon = useShellStore((state) => state.enterDungeon);
|
|
const openGmAdmin = useShellStore((state) => state.openGmAdmin);
|
|
const onlineGroup = useOnlineGroupStore((state) => state.snapshot.group);
|
|
const fillWithAi = useOnlineGroupStore((state) => state.fillWithAi);
|
|
const updateOnlineMember = useOnlineGroupStore((state) => state.updateMember);
|
|
const startOnlineActivity = useOnlineGroupStore((state) => state.startActivity);
|
|
const includeBetaSpecs = useCompanionPartyPreferences((state) => state.includeBetaSpecs);
|
|
const contentState = useContentManagerSnapshot();
|
|
const character = activeCharacter ?? rosterCharacter;
|
|
const isRom = character?.categoryId === "rom";
|
|
const isGm = sessionCanUseGmTools(session);
|
|
const savedSpecialization = specializationById(character?.specializationId);
|
|
const availableRoles = character
|
|
? isRom ? partyRolesForClass(character.classId) : savedSpecialization ? [savedSpecialization.role] : []
|
|
: [];
|
|
|
|
const closeRandomQueue = useCallback(() => {
|
|
if (!queueing) setRandomQueueMode(null);
|
|
}, [queueing]);
|
|
const startRandomQueue = useCallback(async () => {
|
|
if (!randomQueueMode || !selectedRole || queueing) return;
|
|
setQueueing(true);
|
|
setQueueStage("rolling");
|
|
setQueueError("");
|
|
setQueuedDungeonTitle("");
|
|
const result = await queueRandomDungeon({
|
|
mode: randomQueueMode,
|
|
role: selectedRole,
|
|
character,
|
|
session,
|
|
group: onlineGroup,
|
|
includeBetaSpecs,
|
|
fillWithAi,
|
|
onProgress: (stage, dungeon) => { setQueueStage(stage); setQueuedDungeonTitle(dungeon?.title ?? ""); },
|
|
enterDungeon: (dungeonId, role, settings) => enterDungeon(dungeonId, role, settings),
|
|
updateOnlineMember,
|
|
startOnlineActivity: (groupSession, input) => startOnlineActivity(groupSession, input),
|
|
failureNotice: () => useOnlineGroupStore.getState().error || useShellStore.getState().notice,
|
|
});
|
|
if (result.status === "error") setQueueError(result.message);
|
|
else if (result.status === "waiting") {
|
|
useShellStore.getState().setNotice(`Ready as ${PARTY_ROLE_LABELS[selectedRole]}. Waiting for the group leader to start.`);
|
|
setRandomQueueMode(null);
|
|
} else if (result.status === "started") {
|
|
useShellStore.getState().setNotice(`Starting ${result.dungeonTitle} with your group.`);
|
|
setRandomQueueMode(null);
|
|
}
|
|
setQueueing(false);
|
|
}, [character, enterDungeon, fillWithAi, includeBetaSpecs, onlineGroup, queueing, randomQueueMode, selectedRole, session, startOnlineActivity, updateOnlineMember]);
|
|
|
|
const actions = useMemo<MenuAction[]>(() => randomQueueMode ? [
|
|
...availableRoles.map((role) => ({ id: `random-queue-role-${role}`, run: () => setSelectedRole(role), enabled: !queueing })),
|
|
{ id: "random-queue-confirm", run: () => void startRandomQueue(), enabled: Boolean(selectedRole) && !queueing },
|
|
{ id: "random-queue-cancel", run: closeRandomQueue, enabled: !queueing },
|
|
] : gmMenuOpen ? [
|
|
...(isGm ? [{ id: "gm-menu-gm-tools", run: openGmAdmin, neighbors: { up: "gm-menu-close" } }] : []),
|
|
{ id: "gm-menu-boss-testing", run: onOpenBossTesting, ...(!isGm ? { neighbors: { up: "gm-menu-close" } } : {}) },
|
|
{ id: "gm-menu-companion-readiness", run: onOpenReadiness },
|
|
...(isGm ? [{ id: "gm-menu-balance-tuning", run: onOpenBalanceTuning }] : []),
|
|
...(isGm ? [{ id: "gm-menu-balance-lab", run: onOpenBalanceLab }] : []),
|
|
{ id: "gm-menu-reports", run: onOpenReports },
|
|
{ id: "gm-menu-close", run: () => setGmMenuOpen(false), neighbors: { down: isGm ? "gm-menu-gm-tools" : "gm-menu-boss-testing" } },
|
|
] : [
|
|
{
|
|
id: "mode-dungeons",
|
|
run: openDungeons,
|
|
neighbors: { up: "back-to-roster", right: "mode-random-dungeon", down: "mode-random-bite-sized" },
|
|
},
|
|
{
|
|
id: "mode-random-dungeon",
|
|
run: openRandomDungeon,
|
|
neighbors: { up: "back-to-roster", left: "mode-dungeons", down: "mode-randomized-bite-sized" },
|
|
},
|
|
{
|
|
id: "mode-random-bite-sized",
|
|
run: openRandomBiteSizedDungeon,
|
|
neighbors: { up: "mode-dungeons", right: "mode-randomized-bite-sized" },
|
|
},
|
|
{
|
|
id: "mode-randomized-bite-sized",
|
|
run: onOpenRandomizedBiteSized,
|
|
neighbors: { up: "mode-random-dungeon", left: "mode-random-bite-sized" },
|
|
},
|
|
...(character ? [
|
|
{ id: "mode-class-hall", run: openClassHall },
|
|
{ id: "mode-talents", run: () => onOpenEditor("talents") },
|
|
{ id: "mode-keybinds", run: () => onOpenEditor("keybinds") },
|
|
{ id: "mode-bindings", run: () => onOpenEditor("bindings") },
|
|
] : []),
|
|
{ id: "mode-cards", run: onOpenCards },
|
|
{ id: "mode-gm-menu", run: () => setGmMenuOpen(true) },
|
|
...(SHELVED_MODES_ENABLED ? [
|
|
{ id: "mode-manastorms", run: openManastorms },
|
|
{ id: "mode-arenas", run: openArenas },
|
|
] : []),
|
|
{ id: "mode-downloads", run: onOpenDownloads },
|
|
{ id: "back-to-roster", run: returnToCharacters, neighbors: { down: "mode-dungeons" } },
|
|
], [availableRoles, character, closeRandomQueue, gmMenuOpen, isGm, onOpenBalanceLab, onOpenBalanceTuning, onOpenBossTesting, onOpenCards, onOpenDownloads, onOpenEditor, onOpenRandomizedBiteSized, onOpenReadiness, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, openRandomBiteSizedDungeon, openRandomDungeon, queueing, randomQueueMode, returnToCharacters, selectedRole, startRandomQueue]);
|
|
const controller = useMenuController(actions, {
|
|
columns: 2,
|
|
initialId: randomQueueMode ? `random-queue-role-${availableRoles[0]}` : gmMenuOpen ? (isGm ? "gm-menu-gm-tools" : "gm-menu-boss-testing") : "mode-dungeons",
|
|
onBack: randomQueueMode ? closeRandomQueue : gmMenuOpen ? () => setGmMenuOpen(false) : returnToCharacters,
|
|
});
|
|
|
|
const characterClass = character ? classById(character.classId) : null;
|
|
const specialization = specializationById(character?.specializationId);
|
|
const secondaryClass = character?.secondaryClassId ? classById(character.secondaryClassId) : null;
|
|
const gameModeAction = (id: string) => {
|
|
if (id === "manastorms") return openManastorms;
|
|
if (id === "arenas") return openArenas;
|
|
if (id === "downloads") return onOpenDownloads;
|
|
if (id === "random-dungeon") return openRandomDungeon;
|
|
if (id === "random-bite-sized") return openRandomBiteSizedDungeon;
|
|
if (id === "randomized-bite-sized") return onOpenRandomizedBiteSized;
|
|
return () => openDungeons();
|
|
};
|
|
const renderGameMode = (mode: (typeof GAME_MODES)[number]) => 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={gameModeAction(mode.id)}
|
|
>
|
|
<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>
|
|
);
|
|
|
|
return (
|
|
<DualDisplayFrame
|
|
contextLabel="Main menu details"
|
|
top={
|
|
<FrontSurface className="main-menu-screen" ariaLabel="Healer Man main menu">
|
|
<header className="main-menu-header">
|
|
<BrandMark compact tagline={null} />
|
|
<ControllerButton
|
|
controlId="back-to-roster"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
type="button"
|
|
onClick={returnToCharacters}
|
|
>
|
|
Change character
|
|
</ControllerButton>
|
|
</header>
|
|
|
|
<aside className="main-menu-sidebar" aria-label="Character and online group">
|
|
{character && characterClass && (
|
|
<section className="main-menu-character" aria-label="Selected character">
|
|
<span className="main-menu-character__icon" style={{ borderColor: characterClass.color }}>
|
|
<img src={specializationIconUrl(character.classId, character.specializationId)} alt="" aria-hidden="true" />
|
|
</span>
|
|
<div className="main-menu-character__summary">
|
|
<small>Level {character.level}</small>
|
|
<h1>{character.name}</h1>
|
|
<strong style={{ color: characterClass.color }}>
|
|
{characterClass.name}{isRom
|
|
? secondaryClass ? ` / ${secondaryClass.name}` : " / No secondary class"
|
|
: specialization ? ` / ${specialization.displayName}` : ""}
|
|
</strong>
|
|
</div>
|
|
</section>
|
|
)}
|
|
<OnlineGroupPanel defaultOpen />
|
|
</aside>
|
|
|
|
<main className="main-menu-modes">
|
|
<nav className="main-menu-mode-grid" aria-label="Main menu destinations">
|
|
<section className="main-menu-card main-menu-card--adventures" aria-labelledby="main-menu-adventures-title">
|
|
<header><span>01</span><strong id="main-menu-adventures-title">Adventures</strong></header>
|
|
<div className="main-menu-card__grid">
|
|
{GAME_MODES.filter((mode) => ["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"].includes(mode.id)).map(renderGameMode)}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="main-menu-card main-menu-card--secondary" aria-labelledby="main-menu-secondary-title">
|
|
<header><span>02</span><strong id="main-menu-secondary-title">Character & Collection</strong></header>
|
|
<div className="main-menu-card__grid">
|
|
{character && <>
|
|
<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>{isRom ? "Choose a permanent partner class and swap your active ordering." : "Change specialization and retune your heirlooms."}</small></span><em>{isRom && character.level < 10 ? "Unlocks at 10" : "Open"}</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="mode-talents" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={() => onOpenEditor("talents")}>
|
|
<b aria-hidden="true">T</b><span><strong>{isRom ? "Skills" : "Talents"}</strong><small>{isRom ? "Review your primary, secondary, and elite skills." : "View your talent trees, spend points, or reset your build."}</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="mode-keybinds" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={() => onOpenEditor("keybinds")}>
|
|
<b aria-hidden="true">K</b><span><strong>Keybinds</strong><small>View and change keyboard or controller bindings.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="mode-bindings" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={() => onOpenEditor("bindings")}>
|
|
<b aria-hidden="true">A</b><span><strong>Button assignments</strong><small>Choose the abilities on your action bars and controller buttons.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
</>}
|
|
<ControllerButton controlId="mode-cards" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={onOpenCards}>
|
|
<b aria-hidden="true">◆</b><span><strong>Boss Cards</strong><small>Track your collection, rarities, and copies from every dungeon.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="mode-gm-menu" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--available main-menu-mode--gm-menu" type="button" onClick={() => setGmMenuOpen(true)}>
|
|
<b aria-hidden="true">GM</b><span><strong>GM Menu</strong><small>Testing, readiness, reports, and balance tools.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
{GAME_MODES.filter((mode) => !["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"].includes(mode.id)).map(renderGameMode)}
|
|
</div>
|
|
</section>
|
|
</nav>
|
|
{notice && <p className="front-notice main-menu-notice" role="status" aria-live="polite">{notice}</p>}
|
|
</main>
|
|
|
|
<ControllerLegend back />
|
|
{randomQueueMode && (
|
|
<div className="front-modal-backdrop">
|
|
<section className="main-menu-random-modal" role="dialog" aria-modal="true" aria-labelledby="random-queue-title">
|
|
<p className="front-kicker">{randomQueueMode === "regular" ? "Random Dungeon" : "Random Regular Bite-Sized Dungeon"}</p>
|
|
<h2 id="random-queue-title">Choose your role</h2>
|
|
<p>Pick how you want to play, then queue. A dungeon will be rolled and downloaded before you enter.</p>
|
|
<div className="main-menu-random-modal__roles" aria-label="Party role">
|
|
{availableRoles.map((role, index) => (
|
|
<ControllerButton key={role} controlId={`random-queue-role-${role}`}
|
|
selectedId={controller.selectedId} select={controller.select} autoFocus={index === 0}
|
|
className="front-button main-menu-random-modal__role" type="button"
|
|
aria-pressed={selectedRole === role} disabled={queueing} onClick={() => setSelectedRole(role)}>
|
|
{PARTY_ROLE_LABELS[role]}
|
|
</ControllerButton>
|
|
))}
|
|
</div>
|
|
{!availableRoles.length && <p className="front-notice">Choose a specialization in your Class Hall before queuing.</p>}
|
|
{queueing && <p className="main-menu-random-modal__progress" role="status" aria-live="polite">
|
|
{queueStage === "rolling" ? "Rolling a dungeon…" : queueStage === "downloading"
|
|
? `Downloading ${queuedDungeonTitle}${contentState.installingPackId && contentState.totalBytes > 0
|
|
? ` — ${Math.round((contentState.downloadedBytes / contentState.totalBytes) * 100)}%` : "…"}`
|
|
: `Entering ${queuedDungeonTitle}…`}
|
|
</p>}
|
|
{queueError && <p className="front-notice" role="alert">{queueError}</p>}
|
|
<div className="main-menu-random-modal__actions">
|
|
<ControllerButton controlId="random-queue-cancel" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button" type="button" disabled={queueing} onClick={closeRandomQueue}>Cancel</ControllerButton>
|
|
<ControllerButton controlId="random-queue-confirm" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button front-button--primary" type="button" disabled={!selectedRole || queueing}
|
|
onClick={() => void startRandomQueue()}>{queueing ? "Queuing…" : "Queue"}</ControllerButton>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)}
|
|
{gmMenuOpen && (
|
|
<div className="front-modal-backdrop">
|
|
<section className="main-menu-gm-modal" role="dialog" aria-modal="true" aria-labelledby="gm-menu-title">
|
|
<header>
|
|
<div><span>Authorized utilities</span><h2 id="gm-menu-title">GM Menu</h2></div>
|
|
<ControllerButton controlId="gm-menu-close" selectedId={controller.selectedId} select={controller.select} type="button" onClick={() => setGmMenuOpen(false)}>Close</ControllerButton>
|
|
</header>
|
|
<div className="main-menu-gm-grid">
|
|
<ControllerButton controlId="gm-menu-gm-tools" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--gm" type="button" disabled={!isGm} onClick={openGmAdmin}>
|
|
<b aria-hidden="true">GM</b><span><strong>GM Tools</strong><small>Run dungeon simulations, inspect findings, and review recordings.</small></span><em>{isGm ? "Authorized" : "GM account required"}</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="gm-menu-boss-testing" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--gm" type="button" onClick={onOpenBossTesting}>
|
|
<b aria-hidden="true">BT</b><span><strong>Boss Testing</strong><small>Fight a selected or random encounter in an empty testing room.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="gm-menu-companion-readiness" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--gm" type="button" onClick={onOpenReadiness}>
|
|
<b aria-hidden="true">CR</b><span><strong>Companion Readiness</strong><small>Review certified NPC specializations and Playerbots identities.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="gm-menu-balance-tuning" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--gm" type="button" disabled={!isGm} onClick={onOpenBalanceTuning}>
|
|
<b aria-hidden="true">AT</b><span><strong>Ability & Talent Tuning</strong><small>Adjust local cooldowns, costs, damage, healing, durations, and talent modifiers.</small></span><em>{isGm ? "Local" : "GM account required"}</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="gm-menu-balance-lab" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--gm" type="button" disabled={!isGm} onClick={onOpenBalanceLab}>
|
|
<b aria-hidden="true">BL</b><span><strong>Balance Lab</strong><small>Queue automatic party and boss simulations on the server.</small></span><em>{isGm ? "Server" : "GM account required"}</em>
|
|
</ControllerButton>
|
|
<ControllerButton controlId="gm-menu-reports" selectedId={controller.selectedId} select={controller.select}
|
|
className="front-button main-menu-mode main-menu-mode--gm" type="button" onClick={onOpenReports}>
|
|
<b aria-hidden="true">!</b><span><strong>View Reports</strong><small>Browse dungeon issues and follow their status.</small></span><em>Open</em>
|
|
</ControllerButton>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)}
|
|
</FrontSurface>
|
|
}
|
|
bottom={<LazyLocalCompanionPanel />}
|
|
/>
|
|
);
|
|
}
|