From e52a637f9602cf64eb106e11ef91fcc48969b5f7 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 8 Sep 2026 12:13:45 -0400 Subject: [PATCH] feat(minigames): Esc/R reset keybind with hint text for OPORD Recall --- src/components/frontend/opord/OpordGame.tsx | 68 ++++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/src/components/frontend/opord/OpordGame.tsx b/src/components/frontend/opord/OpordGame.tsx index 4a75e5c..f062199 100644 --- a/src/components/frontend/opord/OpordGame.tsx +++ b/src/components/frontend/opord/OpordGame.tsx @@ -265,6 +265,18 @@ function OpordBriefingCanvas({ ); } +function ResetHint({ className }: { className?: string }) { + const keyClass = + "rounded border border-border bg-muted px-1 py-0.5 font-mono text-[10px] leading-none"; + return ( +

+ Press{" "} + Esc or R at any point to + reset the exercise. +

+ ); +} + function Metre({ label, value }: { label: string; value: string | number }) { return (
@@ -353,6 +365,9 @@ export function OpordGame({ submittingRef.current = false; setSubmitting(false); + // Esc/R can reset the run while the submission is in flight; global stats + // stay in sync, but the run-result UI is skipped for a cancelled run. + const cancelled = phaseRef.current !== "quiz"; if (res.success && res.data) { statsRef.current = res.data; onGameRecordedAction(res.data); @@ -364,14 +379,16 @@ export function OpordGame({ ); window.setTimeout(() => router.refresh(), 1700); } - setLastRun({ - correct, - total: preview.total, - score: res.data.score, - xpEarned: res.data.xpEarned, - best: res.data.bestScore, - }); - } else { + if (!cancelled) { + setLastRun({ + correct, + total: preview.total, + score: res.data.score, + xpEarned: res.data.xpEarned, + best: res.data.bestScore, + }); + } + } else if (!cancelled) { setLastRun({ correct, total: preview.total, @@ -381,7 +398,7 @@ export function OpordGame({ error: res.error ?? "Could not record score.", }); } - setPhaseSafe("over"); + if (!cancelled) setPhaseSafe("over"); }, [difficulty, expiredRef, onGameRecordedAction, router, setPhaseSafe]); const startBriefing = useCallback(() => { @@ -411,6 +428,36 @@ export function OpordGame({ setPhaseSafe("quiz"); }, [setPhaseSafe]); + const resetGame = useCallback(() => { + scenarioSeedRef.current = 0; + setScenario(null); + setQuestions([]); + setQuestionIndex(0); + setTypedAnswer(""); + answersRef.current = []; + lastAnswerAtRef.current = 0; + setLastAnswerWasCorrect(null); + setLastRun(null); + setQuizRemainingMs(tier.quizSeconds * 1000); + setPhaseSafe("ready"); + }, [setPhaseSafe, tier.quizSeconds]); + + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return; + const target = event.target; + const typing = + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target instanceof HTMLSelectElement || + (target instanceof HTMLElement && target.isContentEditable); + const key = event.key.toLowerCase(); + if (key === "escape" || (key === "r" && !typing)) resetGame(); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [resetGame]); + const answerCurrent = useCallback( (value: string) => { if (phaseRef.current !== "quiz") return; @@ -484,6 +531,7 @@ export function OpordGame({ +
)} @@ -606,6 +654,8 @@ export function OpordGame({ Your session is about to end. The app will refresh when it expires.

)} + + {phase !== "ready" && } ); }