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.name}{mode.description} Available ) : ( ); return (
Change character
{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

GM Menu

setGmMenuOpen(false)}>Close
GM ToolsRun dungeon simulations, inspect findings, and review recordings.{isGm ? "Authorized" : "GM account required"} Boss TestingFight a selected or random encounter in an empty testing room.Open Companion ReadinessReview certified NPC specializations and Playerbots identities.Open Ability & Talent TuningAdjust local cooldowns, costs, damage, healing, durations, and talent modifiers.{isGm ? "Local" : "GM account required"} Balance LabQueue automatic party and boss simulations on the server.{isGm ? "Server" : "GM account required"} View ReportsBrowse dungeon issues and follow their status.Open
)} } bottom={} /> ); }