1
0
Fork 0

feat(minigames): add UnlockNotice and wire into EOD and radio arenas

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-08 22:38:38 -04:00
parent f76966ebc3
commit 455d705eb9
3 changed files with 66 additions and 0 deletions

View file

@ -4,8 +4,10 @@ import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { Bomb } from "lucide-react";
import type { EodDifficulty, EodStats } from "@/app/(frontend)/eod/actions";
import { isEodUnlocked } from "@/lib/custom-minigames/eod";
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
import { useSidebar } from "@/components/ui/sidebar";
import { UnlockNotice } from "@/components/frontend/minigames/UnlockNotice";
import { EodLeaderboard } from "./EodLeaderboard";
import { MinesweeperBoard } from "./MinesweeperBoard";
@ -25,6 +27,17 @@ export function EodArena({
setPlaying(phase === "playing");
}, []);
const customUnlocked = isEodUnlocked({
bestTrainingTime: initialStats.bestTrainingTime,
bestPerimeterTime: initialStats.bestPerimeterTime,
bestSupplyRoadTime: initialStats.bestSupplyRoadTime,
});
const clearedSectors = [
initialStats.bestTrainingTime,
initialStats.bestPerimeterTime,
initialStats.bestSupplyRoadTime,
].filter((time) => time > 0).length;
useEffect(() => {
if (playing && isMobile) setOpenMobile(false);
}, [isMobile, playing, setOpenMobile]);
@ -38,6 +51,13 @@ export function EodArena({
<Bomb className="size-4" />
Custom minefields
</Link>
{!customUnlocked && (
<UnlockNotice
title="custom minefields"
requirement="you have cleared Training Pad, FOB Perimeter, and Main Supply Road"
progress={`${clearedSectors} of 3 sectors cleared`}
/>
)}
<MinesweeperBoard
initialStats={initialStats}
onGameRecorded={() => setRefreshKey((key) => key + 1)}

View file

@ -0,0 +1,26 @@
import { Lock } from "lucide-react";
/**
* Locked-state hint shown beside a minigame's custom-content link: tells the
* player exactly which milestones unlock authoring user-generated content and
* how close they are. Unlocked players see nothing (the link speaks for itself).
*/
export function UnlockNotice({
title,
requirement,
progress,
}: {
title: string;
requirement: string;
progress: string;
}) {
return (
<p className="flex max-w-xl items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs leading-relaxed text-amber-300">
<Lock className="mt-0.5 size-3.5 shrink-0" />
<span>
<span className="font-medium">Author your own {title}</span> once {requirement}{" "}
<span className="text-amber-200/60">({progress})</span>
</span>
</p>
);
}

View file

@ -4,7 +4,13 @@ import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { Radio } from "lucide-react";
import type { RadioStats } from "@/app/(frontend)/radio/actions";
import {
isRadioUnlocked,
SPRINT_ACCURACY_REQUIREMENT,
WATCH_ACCURACY_REQUIREMENT,
} from "@/lib/custom-minigames/radio";
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
import { UnlockNotice } from "@/components/frontend/minigames/UnlockNotice";
import { RadioLeaderboard } from "./RadioLeaderboard";
import { RadioTraffic } from "./RadioTraffic";
@ -24,6 +30,13 @@ export function RadioArena({
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);
@ -43,6 +56,13 @@ export function RadioArena({
<Radio className="size-4" />
Custom radio tests
</Link>
{!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}