"use client"; import { useCallback, useEffect, useState } from "react"; import { OPORD_DIFFICULTIES, type OpordDifficulty } from "@/lib/opord-recall"; import type { OpordStats } from "@/app/(frontend)/opord/actions"; import { useRealtimePresence } from "@/hooks/useRealtimePresence"; import { useSidebar } from "@/components/ui/sidebar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { cn } from "@/lib/utils"; import { OpordGame } from "./OpordGame"; import { OpordLeaderboard } from "./OpordLeaderboard"; const TIER_OPTIONS = [ { value: "recruit", label: "Recruit", detail: "10 questions · 130s recall · core order" }, { value: "veteran", label: "Veteran", detail: "15 questions · 195s recall · extended order" }, { value: "elite", label: "Elite", detail: "20 questions · 265s recall · full order" }, ] as const satisfies ReadonlyArray<{ readonly value: OpordDifficulty; readonly label: string; readonly detail: string; }>; const TIER_STAT_KEYS = { recruit: "recruitBestScore", veteran: "veteranBestScore", elite: "eliteBestScore", } as const satisfies Record; export function OpordArena({ initialStats, currentUserId, }: { initialStats: OpordStats; currentUserId: number | string; }) { const [stats, setStats] = useState(initialStats); const [refreshKey, setRefreshKey] = useState(0); const [difficulty, setDifficulty] = useState("recruit"); const [playing, setPlaying] = useState(false); const { isMobile, setOpenMobile } = useSidebar(); const activeOperators = useRealtimePresence("opord", playing, true); const handlePhaseChange = useCallback((nextPhase: "ready" | "briefing" | "quiz" | "over") => { setPlaying(nextPhase === "briefing" || nextPhase === "quiz"); }, []); const handleGameRecorded = useCallback((nextStats: OpordStats) => { setStats(nextStats); setRefreshKey((key) => key + 1); }, []); useEffect(() => { if (playing && isMobile) setOpenMobile(false); }, [isMobile, playing, setOpenMobile]); return (
Choose a briefing tier Study the order for as long as you like. Once the recall quiz starts, the clock is unforgiving.
{ if (value === "recruit" || value === "veteran" || value === "elite") { setDifficulty(value); } }} aria-label="OPORD Recall difficulty" className="grid gap-2 md:grid-cols-3" > {TIER_OPTIONS.map((option) => ( ))}
Recall protocol
Study The order covers the objective grid, azimuth, H-HOUR, phase lines, codewords, callsign, and radio frequency. Veteran tiers add garrison tasks, rules of engagement, and a secondary objective; elite adds intelligence targets and a tertiary objective. Study it at your own pace, then press "Begin recall" when you are ready for the quiz.
Recall Questions come one at a time with no going back, and the whole quiz is capped at{" "} {OPORD_DIFFICULTIES[difficulty].quizSeconds} seconds on this tier. Each correct answer is worth {OPORD_DIFFICULTIES[difficulty].correctPoints} points, so speed and accuracy both count.
Debrief A perfect run inside {OPORD_DIFFICULTIES[difficulty].parTimeMs / 1000}s earns a{" "} {OPORD_DIFFICULTIES[difficulty].perfectBonus}-point bonus. Each tier tracks its own best score.
); }