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" && }
);
}