forked from phenom/healer-man
85 lines
5.1 KiB
TypeScript
85 lines
5.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useShellStore } from "../app/shellStore";
|
|
import { BOSS_TESTING_STATUSES, BOSS_TESTING_STATUS_LABELS, useBossTestingReviews, type BossTestingStatus } from "../app/bossTestingReviews";
|
|
import { bossTestIsDefeated } from "../game/BossTestingBridge";
|
|
import { enterBossTesting, prepareBossTesting } from "../app/bossTestingRuntime";
|
|
import { anotherRandomTestingBoss, BOSS_TESTING_CATALOG } from "../game/bossTesting";
|
|
import { useGameStore } from "../game/store";
|
|
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
|
import { ControllerButton } from "./ControllerButton";
|
|
import "./bossTesting.css";
|
|
|
|
export function BossTestingReviewPanel() {
|
|
const boss = useGameStore((state) => state.bossTest);
|
|
const session = useShellStore((state) => state.session);
|
|
const review = useBossTestingReviews((state) => boss ? state.reviews[boss.key] : undefined);
|
|
const save = useBossTestingReviews((state) => state.save);
|
|
const refresh = useBossTestingReviews((state) => state.refresh);
|
|
const [saving, setSaving] = useState(false);
|
|
const [startingRandom, setStartingRandom] = useState(false);
|
|
const [message, setMessage] = useState("");
|
|
const busy = useRef(false);
|
|
const mark = async (status: BossTestingStatus) => {
|
|
if (!boss || busy.current) return;
|
|
busy.current = true; setSaving(true); setMessage("");
|
|
const synced = await save(boss, status, session);
|
|
setMessage(synced ? `Marked ${BOSS_TESTING_STATUS_LABELS[status].toLowerCase()}.` : "Saved on this device. This label will sync when you reconnect or reopen Boss Testing.");
|
|
busy.current = false; setSaving(false);
|
|
};
|
|
useEffect(() => {
|
|
void refresh(session);
|
|
const reconnect = () => void refresh(session);
|
|
window.addEventListener("online", reconnect);
|
|
return () => window.removeEventListener("online", reconnect);
|
|
}, [refresh, session]);
|
|
const defeated = bossTestIsDefeated();
|
|
const disabled = saving || startingRandom;
|
|
const back = () => { if (!busy.current) useGameStore.getState().openOverlay("pause"); };
|
|
const retry = () => useGameStore.getState().resetAtEntrance();
|
|
const leave = () => useShellStore.getState().returnToMainMenu();
|
|
const fightRandom = async () => {
|
|
if (busy.current || !boss) return;
|
|
const next = anotherRandomTestingBoss(BOSS_TESTING_CATALOG, boss.key);
|
|
if (!next) { setMessage("No other bosses are available."); return; }
|
|
busy.current = true; setStartingRandom(true); setMessage(`Preparing ${next.name}…`);
|
|
try {
|
|
await prepareBossTesting(next);
|
|
enterBossTesting(next, useShellStore.getState().activeDungeonRole ?? undefined);
|
|
} catch (failure) {
|
|
setMessage(failure instanceof Error ? failure.message : "The next boss could not be started. Please retry.");
|
|
} finally {
|
|
busy.current = false; setStartingRandom(false);
|
|
}
|
|
};
|
|
const actions: MenuAction[] = [
|
|
...BOSS_TESTING_STATUSES.map((status) => ({ id: `boss-review-${status}`, run: () => void mark(status), enabled: !disabled })),
|
|
{ id: "boss-review-report", run: () => useGameStore.getState().openOverlay("report-issue"), enabled: !disabled },
|
|
{ id: "boss-review-retry", run: retry, enabled: !disabled },
|
|
{ id: "boss-review-random", run: () => void fightRandom(), enabled: !disabled },
|
|
{ id: "boss-review-leave", run: leave, enabled: !disabled },
|
|
{ id: "boss-review-back", run: back, enabled: !disabled },
|
|
];
|
|
const controller = useMenuController(actions, { initialId: "boss-review-needs-testing", onBack: back });
|
|
if (!boss) return null;
|
|
const button = (controlId: string, label: string, run: () => void, pressed?: boolean) =>
|
|
<ControllerButton controlId={controlId} selectedId={controller.selectedId} select={controller.select}
|
|
type="button" className="button" onClick={run} disabled={disabled} aria-pressed={pressed}>{label}</ControllerButton>;
|
|
return <div className="modal-backdrop" role="presentation">
|
|
<section className="pause-menu boss-testing-review" role="dialog" aria-modal="true" aria-labelledby="boss-review-title">
|
|
<p className="eyebrow">{defeated ? "Boss defeated" : "Boss Testing paused"}</p><h2 id="boss-review-title">{boss.name}</h2>
|
|
<p>Current readiness: <strong>{BOSS_TESTING_STATUS_LABELS[review?.status ?? "needs-testing"]}</strong></p>
|
|
<div className="boss-testing-actions" aria-label="Boss readiness">{BOSS_TESTING_STATUSES.map((status) =>
|
|
<span key={status}>{button(`boss-review-${status}`, `Mark ${BOSS_TESTING_STATUS_LABELS[status].toLowerCase()}`, () => void mark(status), review?.status === status)}</span>
|
|
)}</div>
|
|
{message && <p role="status">{message}</p>}
|
|
<div className="pause-menu__actions">
|
|
{button("boss-review-report", "Submit a boss report", () => useGameStore.getState().openOverlay("report-issue"))}
|
|
{button("boss-review-retry", "Fight this boss again", retry)}
|
|
{button("boss-review-random", startingRandom ? "Preparing random boss…" : "Fight another random boss", () => void fightRandom())}
|
|
{button("boss-review-leave", "Return to main menu", leave)}
|
|
{button("boss-review-back", "Back to pause menu", back)}
|
|
</div>
|
|
</section>
|
|
</div>;
|
|
}
|