import { useEffect, useMemo, useState } from "react"; import { deleteDungeonSimulationReplay, fetchDungeonSimulationReplay, fetchDungeonSimulationReplays, fetchDungeonSimulationReport, fetchGmDungeonRoutes, putDungeonRouteReview, } from "../app/dungeonSimulationClient"; import { enterDungeonSimulation, enterDungeonSimulationReplay } from "../app/dungeonSimulationRuntime"; import { sessionCanUseGmTools } from "../app/releaseFeatures"; import { useShellStore } from "../app/shellStore"; import { DUNGEON_CATALOG } from "../game/dungeonCatalog"; import { DUNGEON_ROUTE_CATALOG } from "../game/dungeonRoutes"; import type { DungeonReplaySummary, DungeonRouteReview, DungeonRouteReviewStatus, DungeonSimulationMode, DungeonSimulationReportV1, } from "../game/dungeonSimulationTypes"; import { BrandMark, FrontSurface } from "./FrontSurface"; function saveReport(report: DungeonSimulationReportV1): void { const url = URL.createObjectURL(new Blob([JSON.stringify(report, null, 2)], { type: "application/json" })); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `${report.dungeonId}-${report.seed}-SimulationReportV1.json`; anchor.click(); URL.revokeObjectURL(url); } export function DungeonSimulationAdminScreen() { const session = useShellStore((state) => state.session); const returnToMainMenu = useShellStore((state) => state.returnToMainMenu); const [selectedDungeonId, setSelectedDungeonId] = useState(DUNGEON_ROUTE_CATALOG.routes[0]?.dungeonId ?? ""); const [mode, setMode] = useState("navigation-only"); const [seed, setSeed] = useState("route-review-001"); const [reviews, setReviews] = useState([]); const [replays, setReplays] = useState([]); const [reviewStatus, setReviewStatus] = useState("Candidate"); const [reviewNote, setReviewNote] = useState(""); const [overrideNote, setOverrideNote] = useState(""); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const route = DUNGEON_ROUTE_CATALOG.routes.find((candidate) => candidate.dungeonId === selectedDungeonId) ?? DUNGEON_ROUTE_CATALOG.routes[0]; const dungeon = DUNGEON_CATALOG.find((candidate) => candidate.id === route?.dungeonId); const storedReview = reviews.find((candidate) => candidate.dungeonId === route?.dungeonId && candidate.routeId === route?.id) ?? null; const review = storedReview?.routeHash === route?.hash ? storedReview : null; const routeReplays = useMemo(() => replays.filter((replay) => ( replay.dungeonId === route?.dungeonId && replay.routeId === route?.id && replay.routeHash === route?.hash )), [replays, route?.dungeonId, route?.hash, route?.id]); const refresh = async () => { if (!sessionCanUseGmTools(session)) return; setBusy(true); try { const [routes, recordings] = await Promise.all([ fetchGmDungeonRoutes(session), fetchDungeonSimulationReplays(session), ]); setReviews(routes.reviews); setReplays(recordings); setMessage(""); } catch (error) { setMessage(error instanceof Error ? error.message : "Unable to load route reviews."); } finally { setBusy(false); } }; useEffect(() => { void refresh(); }, [session]); useEffect(() => { setReviewStatus(review?.status ?? (route?.generationStatus === "Rejected" ? "NeedsWork" : "Candidate")); setReviewNote(review?.note ?? ""); setOverrideNote(review?.criticalOverrideNote ?? ""); }, [review?.revision, route?.hash]); if (!sessionCanUseGmTools(session)) { return (

Game Master access required

Dungeon simulation artifacts require a server-authenticated GM account.

); } const saveReview = async () => { if (!route) return; setBusy(true); try { const updated = await putDungeonRouteReview(session, { dungeonId: route.dungeonId, routeId: route.id, routeHash: route.hash, }, { status: reviewStatus, expectedRevision: storedReview?.revision ?? 0, note: reviewNote, criticalOverrideNote: overrideNote, }); setReviews((current) => [...current.filter((candidate) => !( candidate.dungeonId === updated.dungeonId && candidate.routeId === updated.routeId )), updated]); setMessage(`Review saved at revision ${updated.revision}.`); } catch (error) { setMessage(error instanceof Error ? error.message : "Unable to save review."); } finally { setBusy(false); } }; const openReplay = async (id: string) => { setBusy(true); try { const replay = await fetchDungeonSimulationReplay(session, id); if (!enterDungeonSimulationReplay(replay)) setMessage("The replay could not be opened."); } catch (error) { setMessage(error instanceof Error ? error.message : "Unable to load replay."); setBusy(false); } }; const removeReplay = async (id: string) => { setBusy(true); try { await deleteDungeonSimulationReplay(session, id); setReplays((current) => current.filter((candidate) => candidate.id !== id)); setMessage("Recording deleted."); } catch (error) { setMessage(error instanceof Error ? error.message : "Unable to delete replay."); } finally { setBusy(false); } }; const downloadReplayReport = async (id: string) => { setBusy(true); try { saveReport(await fetchDungeonSimulationReport(session, id)); setMessage(""); } catch (error) { setMessage(error instanceof Error ? error.message : "Unable to download report."); } finally { setBusy(false); } }; return (

Game Master tools

Dungeon route simulations

{review?.status ?? (route?.generationStatus === "Rejected" ? "NeedsWork" : "Candidate")} {dungeon?.title ?? route?.dungeonId} {route?.hash} {route?.legs.length ?? 0} required legs · {route?.legs.reduce((sum, leg) => sum + leg.checkpoints.length, 0) ?? 0} checkpoints {route?.generationStatus === "Rejected" && Offline rejection: {route.generationIssues.join(", ")}}
Launch live simulation
Exact-hash review