"use client"; import { useCallback, useEffect, useState } from "react"; import { AIM_TRAINER_DIFFICULTIES, isAimTrainerDifficulty, type AimTrainerDifficulty, } from "@/lib/minigames/aim-trainer"; import type { AimTrainerStats } from "@/app/(frontend)/minigames/qualification/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 { AimTrainerGame, type AimTrainerPhase } from "./AimTrainerGame"; import { AimTrainerLeaderboard } from "./AimTrainerLeaderboard"; const TIER_OPTIONS = [ { value: "recruit", label: "Recruit", detail: "Steady silhouettes" }, { value: "veteran", label: "Veteran", detail: "Moving range" }, { value: "elite", label: "Elite", detail: "High-speed range" }, ] as const satisfies ReadonlyArray<{ readonly value: AimTrainerDifficulty; readonly label: string; readonly detail: string; }>; const TIER_STAT_KEYS = { recruit: { bestScore: "recruitBestScore", bestAccuracy: "recruitBestAccuracy" }, veteran: { bestScore: "veteranBestScore", bestAccuracy: "veteranBestAccuracy" }, elite: { bestScore: "eliteBestScore", bestAccuracy: "eliteBestAccuracy" }, } as const satisfies Record< AimTrainerDifficulty, { readonly bestScore: "recruitBestScore" | "veteranBestScore" | "eliteBestScore"; readonly bestAccuracy: "recruitBestAccuracy" | "veteranBestAccuracy" | "eliteBestAccuracy"; } >; export function AimTrainerArena({ initialStats, currentUserId, }: { readonly initialStats: AimTrainerStats; readonly 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 activeTrainees = useRealtimePresence("qualification", playing, true); const handlePhaseChange = useCallback((nextPhase: AimTrainerPhase) => { setPlaying(nextPhase === "playing"); }, []); const handleGameRecorded = useCallback((nextStats: AimTrainerStats) => { setStats(nextStats); setRefreshKey((key) => key + 1); }, []); useEffect(() => { if (playing && isMobile) setOpenMobile(false); }, [isMobile, playing, setOpenMobile]); return (
Choose a qualification lane Each course lasts 30 seconds. Accuracy keeps your score climbing when the range gets busy.
{ if (isAimTrainerDifficulty(value)) setDifficulty(value); }} aria-label="Qualification difficulty" className="grid gap-2 md:grid-cols-3" > {TIER_OPTIONS.map((option) => { const statKeys = TIER_STAT_KEYS[option.value]; const optionStats = { bestScore: stats[statKeys.bestScore], bestAccuracy: stats[statKeys.bestAccuracy], }; return ( ); })}
Qualification protocol
Acquire Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile silhouettes are active at once, plus an occasional blue civilian that never counts toward the limit. Targets rebound off each other and the range walls.
Prioritize Headshots score 250 points. Torso hits score 100 points. Blue civilians cost 100 points, so never shoot them.
Qualify The course adds an accuracy bonus and records the best score for each lane. Current target window: {AIM_TRAINER_DIFFICULTIES[difficulty].targetLifetimeMs / 1000}s.
); }