1
0
Fork 0

Compare commits

...

2 commits

2 changed files with 60 additions and 10 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "polaris-task-force", "name": "polaris-task-force",
"version": "0.2.20", "version": "0.2.21",
"description": "Web app for the Arma 3 unit Polaris Task Force. Uses Next.js for client- and server-side rendering, with a Postgres database and some other neat libraries.", "description": "Web app for the Arma 3 unit Polaris Task Force. Uses Next.js for client- and server-side rendering, with a Postgres database and some other neat libraries.",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View file

@ -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 (
<p className={cn("text-xs text-muted-foreground", className)}>
Press{" "}
<kbd className={keyClass}>Esc</kbd> or <kbd className={keyClass}>R</kbd> at any point to
reset the exercise.
</p>
);
}
function Metre({ label, value }: { label: string; value: string | number }) { function Metre({ label, value }: { label: string; value: string | number }) {
return ( return (
<div className="rounded-lg border border-border bg-background px-4 py-3 text-center"> <div className="rounded-lg border border-border bg-background px-4 py-3 text-center">
@ -353,6 +365,9 @@ export function OpordGame({
submittingRef.current = false; submittingRef.current = false;
setSubmitting(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) { if (res.success && res.data) {
statsRef.current = res.data; statsRef.current = res.data;
onGameRecordedAction(res.data); onGameRecordedAction(res.data);
@ -364,6 +379,7 @@ export function OpordGame({
); );
window.setTimeout(() => router.refresh(), 1700); window.setTimeout(() => router.refresh(), 1700);
} }
if (!cancelled) {
setLastRun({ setLastRun({
correct, correct,
total: preview.total, total: preview.total,
@ -371,7 +387,8 @@ export function OpordGame({
xpEarned: res.data.xpEarned, xpEarned: res.data.xpEarned,
best: res.data.bestScore, best: res.data.bestScore,
}); });
} else { }
} else if (!cancelled) {
setLastRun({ setLastRun({
correct, correct,
total: preview.total, total: preview.total,
@ -381,7 +398,7 @@ export function OpordGame({
error: res.error ?? "Could not record score.", error: res.error ?? "Could not record score.",
}); });
} }
setPhaseSafe("over"); if (!cancelled) setPhaseSafe("over");
}, [difficulty, expiredRef, onGameRecordedAction, router, setPhaseSafe]); }, [difficulty, expiredRef, onGameRecordedAction, router, setPhaseSafe]);
const startBriefing = useCallback(() => { const startBriefing = useCallback(() => {
@ -411,6 +428,36 @@ export function OpordGame({
setPhaseSafe("quiz"); setPhaseSafe("quiz");
}, [setPhaseSafe]); }, [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( const answerCurrent = useCallback(
(value: string) => { (value: string) => {
if (phaseRef.current !== "quiz") return; if (phaseRef.current !== "quiz") return;
@ -484,6 +531,7 @@ export function OpordGame({
<Button type="button" size="lg" onClick={startBriefing} disabled={expired}> <Button type="button" size="lg" onClick={startBriefing} disabled={expired}>
Receive briefing Receive briefing
</Button> </Button>
<ResetHint />
</div> </div>
)} )}
@ -606,6 +654,8 @@ export function OpordGame({
Your session is about to end. The app will refresh when it expires. Your session is about to end. The app will refresh when it expires.
</p> </p>
)} )}
{phase !== "ready" && <ResetHint className="text-center" />}
</div> </div>
); );
} }