Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
101 lines
4.1 KiB
TypeScript
101 lines
4.1 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import type { RadioStats } from "@/app/(frontend)/minigames/radio/actions";
|
||
import {
|
||
isRadioUnlocked,
|
||
SPRINT_ACCURACY_REQUIREMENT,
|
||
WATCH_ACCURACY_REQUIREMENT,
|
||
} from "@/lib/minigames/custom/radio";
|
||
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
|
||
import { UnlockNotice } from "@/components/frontend/minigames/UnlockNotice";
|
||
import { UgcLinkButton } from "@/components/frontend/minigames/UgcLinkButton";
|
||
import { RadioLeaderboard } from "./RadioLeaderboard";
|
||
import { RadioTraffic } from "./RadioTraffic";
|
||
|
||
export function RadioArena({
|
||
initialStats,
|
||
currentUserId,
|
||
}: {
|
||
initialStats: RadioStats;
|
||
currentUserId: number | string;
|
||
}) {
|
||
const [refreshKey, setRefreshKey] = useState(0);
|
||
const [playing, setPlaying] = useState(false);
|
||
const [mode, setMode] = useState<"sprint" | "watch">("sprint");
|
||
const [modeHydrated, setModeHydrated] = useState(false);
|
||
const activeOperators = useRealtimePresence("radio", playing, true);
|
||
const onPhaseChange = useCallback((phase: "ready" | "playing" | "over") => {
|
||
setPlaying(phase === "playing");
|
||
}, []);
|
||
|
||
const customUnlocked = isRadioUnlocked({
|
||
sprintBestAccuracy: initialStats.sprintBestAccuracy,
|
||
watchBestAccuracy: initialStats.watchBestAccuracy,
|
||
});
|
||
const sprintPercent = Math.round(initialStats.sprintBestAccuracy * 100);
|
||
const watchPercent = Math.round(initialStats.watchBestAccuracy * 100);
|
||
|
||
useEffect(() => {
|
||
const savedMode = window.localStorage.getItem("radio-traffic-mode");
|
||
if (savedMode === "sprint" || savedMode === "watch") setMode(savedMode);
|
||
setModeHydrated(true);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (modeHydrated) window.localStorage.setItem("radio-traffic-mode", mode);
|
||
}, [mode, modeHydrated]);
|
||
|
||
return (
|
||
<div className="flex flex-col items-center gap-6">
|
||
{customUnlocked && (
|
||
<UgcLinkButton
|
||
href="/minigames/radio/custom"
|
||
title="UGC Radio Tests"
|
||
subtitle="Play and create custom radio tests you can share with other unit members! Runs are unranked, so it is a great place to practice before an official transmission."
|
||
unlocked={customUnlocked}
|
||
className="max-w-[520px]"
|
||
/>
|
||
)}
|
||
{!customUnlocked && (
|
||
<UnlockNotice
|
||
title="custom radio tests"
|
||
requirement={`your best accuracy reaches ${Math.round(SPRINT_ACCURACY_REQUIREMENT * 100)}% in sprint and ${Math.round(WATCH_ACCURACY_REQUIREMENT * 100)}% in watch`}
|
||
progress={`Sprint ${sprintPercent}%, Watch ${watchPercent}%`}
|
||
/>
|
||
)}
|
||
<RadioTraffic
|
||
initialStats={initialStats}
|
||
mode={mode}
|
||
onModeChangeAction={setMode}
|
||
onGameRecordedAction={() => setRefreshKey((key) => key + 1)}
|
||
onPhaseChangeAction={onPhaseChange}
|
||
/>
|
||
<div className="w-full max-w-[520px]">
|
||
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">How scoring works</h2>
|
||
<ul className="mt-1.5 flex flex-col gap-0.5 text-xs leading-relaxed text-muted-foreground/80">
|
||
<li>
|
||
Accuracy tracks every keystroke; each wrong character or backspace counts against it. Each message scores accuracy × speed multiplier, up to ~120 points for a fast perfect message.
|
||
</li>
|
||
<li>
|
||
<span className="font-medium text-foreground/80">Sprint:</span> clear exactly 10 messages as fast as possible; maximum score is 1200.
|
||
</li>
|
||
<li>
|
||
<span className="font-medium text-foreground/80">Watch:</span> relay as many messages as possible in 60 seconds; score is unbounded in practice.
|
||
</li>
|
||
<li>
|
||
<span className="font-medium text-foreground/80">XP:</span> earn 0.1 per perfectly-typed message.
|
||
</li>
|
||
<li>The lifetime leaderboard ranks Top Score, Top Accuracy, Fastest Sprint, and Most Messages.</li>
|
||
</ul>
|
||
</div>
|
||
<RadioLeaderboard
|
||
refreshKey={refreshKey}
|
||
mode={mode}
|
||
modeHydrated={modeHydrated}
|
||
currentUserId={currentUserId}
|
||
activeOperators={activeOperators}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|