171 lines
9.4 KiB
TypeScript
171 lines
9.4 KiB
TypeScript
import { useMemo } from "react";
|
|
import { useShellStore } from "../app/shellStore";
|
|
import { useGameStore } from "../game/store";
|
|
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
|
import { ControllerButton } from "./ControllerButton";
|
|
import { sessionCanUseShelvedGmTools } from "../app/releaseFeatures";
|
|
import { DungeonIssueReportPanel } from "./DungeonIssueReportPanel";
|
|
import { BossTestingReviewPanel } from "./BossTestingReviewPanel";
|
|
import { classById } from "../app/characterCatalog";
|
|
import { specializationById, specializationIconUrl } from "../game/specializationCatalog";
|
|
import { dungeonBossCards } from "../game/dungeonBossCards";
|
|
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
|
import { DungeonBossCards } from "./DungeonBossCards";
|
|
import { useDungeonRaceStore } from "../app/dungeonRaceStore";
|
|
|
|
function PauseDialog() {
|
|
const closeOverlay = useGameStore((state) => state.closeOverlay);
|
|
const openOverlay = useGameStore((state) => state.openOverlay);
|
|
const reset = useGameStore((state) => state.resetAtEntrance);
|
|
const gameMode = useGameStore((state) => state.gameMode);
|
|
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
|
const activeDifficultyId = useGameStore((state) => state.activeDifficultyId);
|
|
const sessionRevision = useGameStore((state) => state.sessionRevision);
|
|
const bossTest = useGameStore((state) => state.bossTest);
|
|
const bossCards = useShellStore((state) => state.bossCards);
|
|
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
|
const session = useShellStore((state) => state.session);
|
|
const returnToMainMenu = useShellStore((state) => state.returnToMainMenu);
|
|
const abandonManastormRun = useShellStore((state) => state.abandonManastormRun);
|
|
|
|
const resume = () => {
|
|
closeOverlay();
|
|
};
|
|
const returnToEntrance = () => {
|
|
if (useDungeonRaceStore.getState().inGame) {
|
|
void useDungeonRaceStore.getState().leave();
|
|
useGameStore.setState({ raceStartBlocked: false });
|
|
}
|
|
reset();
|
|
};
|
|
const leaveDungeon = () => {
|
|
if (useDungeonRaceStore.getState().inGame) void useDungeonRaceStore.getState().leave();
|
|
closeOverlay();
|
|
returnToMainMenu();
|
|
};
|
|
const abandonManastorm = () => {
|
|
closeOverlay();
|
|
abandonManastormRun();
|
|
};
|
|
const skillMenuLabel = activeCharacter?.categoryId === "rom" ? "Skills" : "Talents";
|
|
const isGm = sessionCanUseShelvedGmTools(session);
|
|
const characterClass = activeCharacter ? classById(activeCharacter.classId) : null;
|
|
const specialization = specializationById(activeCharacter?.specializationId);
|
|
const classDetail = activeCharacter?.secondaryClassId
|
|
? classById(activeCharacter.secondaryClassId).name
|
|
: specialization && specialization.classId === activeCharacter?.classId ? specialization.displayName : null;
|
|
const dungeonCards = useMemo(() => gameMode === "dungeon"
|
|
? dungeonBossCards(requireDungeonDefinition(activeDungeonId), activeDifficultyId, bossCards)
|
|
: [], [activeDungeonId, activeDifficultyId, bossCards, gameMode, sessionRevision]);
|
|
|
|
const actions = useMemo<MenuAction[]>(() => [
|
|
{ id: "pause-resume", run: resume, neighbors: { down: "pause-party", right: "pause-party", up: "pause-main-menu", left: "pause-main-menu" } },
|
|
{ id: "pause-party", run: () => openOverlay("party"), neighbors: { up: "pause-resume" } },
|
|
{ id: "pause-map", run: () => openOverlay("map") },
|
|
{ id: "pause-boss-guide", run: () => openOverlay("boss-guide") },
|
|
{ id: "pause-boss-cards", run: () => openOverlay("boss-cards") },
|
|
{ id: "pause-inventory", run: () => openOverlay("inventory") },
|
|
{ id: "pause-spellbook", run: () => openOverlay("spellbook") },
|
|
{ id: "pause-talents", run: () => openOverlay("talents") },
|
|
{ id: "pause-bindings", run: () => openOverlay("bindings") },
|
|
{ id: "pause-options", run: () => openOverlay("options") },
|
|
{ id: "pause-keybinds", run: () => openOverlay("keybinds") },
|
|
{ id: "pause-ui-layout", run: () => openOverlay("ui-layout") },
|
|
...(isGm ? [{ id: "pause-gm", run: () => openOverlay("gm") }] : []),
|
|
...(gameMode === "dungeon" ? [{ id: "pause-report-issue", run: () => openOverlay("report-issue") }] : []),
|
|
...(bossTest ? [{ id: "pause-boss-testing-review", run: () => openOverlay("boss-test-review") }] : []),
|
|
...(gameMode === "manastorm"
|
|
? [{ id: "pause-abandon-manastorm", run: abandonManastorm }]
|
|
: [{ id: "pause-entrance", run: returnToEntrance }]),
|
|
{ id: "pause-main-menu", run: leaveDungeon },
|
|
], [abandonManastormRun, bossTest, closeOverlay, gameMode, isGm, openOverlay, reset, returnToMainMenu]);
|
|
const controller = useMenuController(actions, { columns: 2, initialId: "pause-resume", onBack: resume });
|
|
|
|
const menuButton = (id: string, label: string, run: () => void, primary = false) => (
|
|
<ControllerButton
|
|
controlId={id}
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
type="button"
|
|
className={`button ${primary ? "button--primary" : ""}`}
|
|
onClick={run}
|
|
>{label}</ControllerButton>
|
|
);
|
|
|
|
return (
|
|
<div className="modal-backdrop" role="presentation">
|
|
<section className="pause-menu pause-menu--hub" role="dialog" aria-modal="true" aria-labelledby="pause-title">
|
|
<header className="pause-menu__header">
|
|
<h2 id="pause-title">Paused</h2>
|
|
<div className="pause-menu__character">
|
|
{activeCharacter && <img
|
|
src={specializationIconUrl(activeCharacter.classId, activeCharacter.specializationId)}
|
|
alt=""
|
|
aria-hidden="true"
|
|
style={{ borderColor: characterClass?.color }}
|
|
/>}
|
|
<div>
|
|
<strong>{activeCharacter?.name ?? "Adventurer"}</strong>
|
|
<span>Level {activeCharacter?.level ?? 1} · {characterClass?.name ?? "Explorer"}{classDetail ? ` / ${classDetail}` : ""}</span>
|
|
</div>
|
|
</div>
|
|
{bossTest && <p className="pause-menu__context">Boss Testing · {bossTest.name}</p>}
|
|
</header>
|
|
{gameMode === "dungeon" && <section className="pause-menu__bosses" aria-label="Boss cards in this dungeon">
|
|
<div className="pause-menu__bosses-heading"><strong>Boss cards in this dungeon</strong>
|
|
<span>{dungeonCards.filter((card) => card.copies > 0).length} / {dungeonCards.length} collected</span></div>
|
|
<DungeonBossCards cards={dungeonCards} compact />
|
|
</section>}
|
|
<div className="pause-menu__resume">{menuButton("pause-resume", "Resume game", resume, true)}</div>
|
|
<div className="pause-menu__groups">
|
|
<section className="pause-menu__group" aria-labelledby="pause-adventure-title">
|
|
<h3 id="pause-adventure-title">Adventure</h3>
|
|
<div className="pause-menu__actions">
|
|
{menuButton("pause-party", "Party management", () => openOverlay("party"))}
|
|
{menuButton("pause-map", "Dungeon map", () => openOverlay("map"))}
|
|
{menuButton("pause-boss-guide", "Boss guide", () => openOverlay("boss-guide"))}
|
|
{menuButton("pause-boss-cards", "Boss cards", () => openOverlay("boss-cards"))}
|
|
</div>
|
|
</section>
|
|
<section className="pause-menu__group" aria-labelledby="pause-character-title">
|
|
<h3 id="pause-character-title">Character</h3>
|
|
<div className="pause-menu__actions">
|
|
{menuButton("pause-inventory", "Inventory & equipment", () => openOverlay("inventory"))}
|
|
{menuButton("pause-spellbook", "Spellbook", () => openOverlay("spellbook"))}
|
|
{menuButton("pause-talents", skillMenuLabel, () => openOverlay("talents"))}
|
|
{menuButton("pause-bindings", "Ability controls", () => openOverlay("bindings"))}
|
|
</div>
|
|
</section>
|
|
<section className="pause-menu__group" aria-labelledby="pause-settings-title">
|
|
<h3 id="pause-settings-title">Settings</h3>
|
|
<div className="pause-menu__actions">
|
|
{menuButton("pause-options", "Options", () => openOverlay("options"))}
|
|
{menuButton("pause-keybinds", "Keybinds", () => openOverlay("keybinds"))}
|
|
{menuButton("pause-ui-layout", "Edit UI & templates", () => openOverlay("ui-layout"))}
|
|
{isGm && menuButton("pause-gm", "GM placement editor", () => openOverlay("gm"))}
|
|
</div>
|
|
</section>
|
|
<section className="pause-menu__group" aria-labelledby="pause-session-title">
|
|
<h3 id="pause-session-title">Session</h3>
|
|
<div className="pause-menu__actions">
|
|
{gameMode === "dungeon" && menuButton("pause-report-issue", "Report an issue", () => openOverlay("report-issue"))}
|
|
{bossTest && menuButton("pause-boss-testing-review", "Boss readiness & report", () => openOverlay("boss-test-review"))}
|
|
{gameMode === "manastorm"
|
|
? menuButton("pause-abandon-manastorm", "Abandon Manastorm run", abandonManastorm)
|
|
: menuButton("pause-entrance", bossTest ? "Restart boss test" : "Return to entrance", returnToEntrance)}
|
|
{menuButton("pause-main-menu", "Main menu", leaveDungeon)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PauseMenu() {
|
|
const overlay = useGameStore((state) => state.overlay);
|
|
if (overlay === "report-issue") return <DungeonIssueReportPanel />;
|
|
if (overlay === "boss-test-review") return <BossTestingReviewPanel />;
|
|
return overlay === "pause" ? <PauseDialog /> : null;
|
|
}
|