import { lazy, Suspense, useMemo, useState } from "react";
import { classById } from "../app/characterCatalog";
import { selectedCharacter, useShellStore } from "../app/shellStore";
import { DualDisplayFrame } from "../components/DualDisplayFrame";
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: "randomized-bite-sized",
number: "02",
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(null);
if (editor) return setEditor(null)} />;
if (bossTestingOpen) return Opening Boss Testing…
}> setBossTestingOpen(false)} />;
if (balanceLabOpen) return Opening Balance Lab…}> setBalanceLabOpen(false)} /> ;
if (balanceTuningOpen) return Opening balance tuning…}> setBalanceTuningOpen(false)} /> ;
if (readinessOpen) return Opening Companion Readiness…}> setReadinessOpen(false)} /> ;
if (randomizedBiteSizedOpen) return Opening Randomized Bite-Sized…}> setRandomizedBiteSizedOpen(false)} /> ;
if (cardsOpen) return Opening collection log…}> setCardsOpen(false)} /> ;
if (contentManagerOpen) return setContentManagerOpen(false)} />;
if (reportsOpen) return setReportsOpen(false)} />;
return 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 activeCharacter = useShellStore((state) => state.activeCharacter);
const rosterCharacter = useShellStore(selectedCharacter);
const openDungeons = useShellStore((state) => state.openDungeons);
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 openGmAdmin = useShellStore((state) => state.openGmAdmin);
const character = activeCharacter ?? rosterCharacter;
const isRom = character?.categoryId === "rom";
const isGm = sessionCanUseGmTools(session);
const actions = useMemo(() => 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-randomized-bite-sized" },
},
{
id: "mode-randomized-bite-sized",
run: onOpenRandomizedBiteSized,
neighbors: { up: "back-to-roster", left: "mode-dungeons" },
},
...(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" } },
], [character, gmMenuOpen, isGm, onOpenBalanceLab, onOpenBalanceTuning, onOpenBossTesting, onOpenCards, onOpenDownloads, onOpenEditor, onOpenRandomizedBiteSized, onOpenReadiness, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, returnToCharacters]);
const controller = useMenuController(actions, {
columns: 2,
initialId: gmMenuOpen ? (isGm ? "gm-menu-gm-tools" : "gm-menu-boss-testing") : "mode-dungeons",
onBack: 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 renderGameMode = (mode: (typeof GAME_MODES)[number]) => mode.available ? (
{mode.number}
{mode.name} {mode.description}
Available
) : (
{mode.number}
{mode.name} {mode.description}
Coming soon
);
return (
{GAME_MODES.filter((mode) => mode.id === "dungeons" || mode.id === "randomized-bite-sized").map(renderGameMode)}
{character && <>
CH Class Hall {isRom ? "Choose a permanent partner class and swap your active ordering." : "Change specialization and retune your heirlooms."} {isRom && character.level < 10 ? "Unlocks at 10" : "Open"}
onOpenEditor("talents")}>
T {isRom ? "Skills" : "Talents"} {isRom ? "Review your primary, secondary, and elite skills." : "View your talent trees, spend points, or reset your build."} Open
onOpenEditor("keybinds")}>
K Keybinds View and change keyboard or controller bindings. Open
onOpenEditor("bindings")}>
A Button assignments Choose the abilities on your action bars and controller buttons. Open
>}
◆ Boss Cards Track your collection, rarities, and copies from every dungeon. Open
setGmMenuOpen(true)}>
GM GM Menu Testing, readiness, reports, and balance tools. Open
{GAME_MODES.filter((mode) => mode.id !== "dungeons" && mode.id !== "randomized-bite-sized").map(renderGameMode)}
{notice && {notice}
}
{gmMenuOpen && (
Authorized utilities
setGmMenuOpen(false)}>Close
GM GM Tools Run dungeon simulations, inspect findings, and review recordings. {isGm ? "Authorized" : "GM account required"}
BT Boss Testing Fight a selected or random encounter in an empty testing room. Open
CR Companion Readiness Review certified NPC specializations and Playerbots identities. Open
AT Ability & Talent Tuning Adjust local cooldowns, costs, damage, healing, durations, and talent modifiers. {isGm ? "Local" : "GM account required"}
BL Balance Lab Queue automatic party and boss simulations on the server. {isGm ? "Server" : "GM account required"}
! View Reports Browse dungeon issues and follow their status. Open
)}
}
bottom={ }
/>
);
}