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

{notice}

}
{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={} /> ); }