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(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 [randomQueueMode, setRandomQueueMode] = useState(null);
const [selectedRole, setSelectedRole] = useState(null);
const [queueing, setQueueing] = useState(false);
const [queueStage, setQueueStage] = useState("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(() => 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 ? (
{mode.number}
{mode.name} {mode.description}
Available
) : (
{mode.number}
{mode.name} {mode.description}
Coming soon
);
return (
{GAME_MODES.filter((mode) => ["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"].includes(mode.id)).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) => !["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"].includes(mode.id)).map(renderGameMode)}
{notice && {notice}
}
{randomQueueMode && (
{randomQueueMode === "regular" ? "Random Dungeon" : "Random Regular Bite-Sized Dungeon"}
Choose your role
Pick how you want to play, then queue. A dungeon will be rolled and downloaded before you enter.
{availableRoles.map((role, index) => (
setSelectedRole(role)}>
{PARTY_ROLE_LABELS[role]}
))}
{!availableRoles.length && Choose a specialization in your Class Hall before queuing.
}
{queueing &&
{queueStage === "rolling" ? "Rolling a dungeon…" : queueStage === "downloading"
? `Downloading ${queuedDungeonTitle}${contentState.installingPackId && contentState.totalBytes > 0
? ` — ${Math.round((contentState.downloadedBytes / contentState.totalBytes) * 100)}%` : "…"}`
: `Entering ${queuedDungeonTitle}…`}
}
{queueError && {queueError}
}
Cancel
void startRandomQueue()}>{queueing ? "Queuing…" : "Queue"}
)}
{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={ }
/>
);
}