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:
parent
f76966ebc3
commit
455d705eb9
3 changed files with 66 additions and 0 deletions
|
|
@ -4,8 +4,10 @@ import { useCallback, useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Bomb } from "lucide-react";
|
import { Bomb } from "lucide-react";
|
||||||
import type { EodDifficulty, EodStats } from "@/app/(frontend)/eod/actions";
|
import type { EodDifficulty, EodStats } from "@/app/(frontend)/eod/actions";
|
||||||
|
import { isEodUnlocked } from "@/lib/custom-minigames/eod";
|
||||||
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
|
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
|
import { UnlockNotice } from "@/components/frontend/minigames/UnlockNotice";
|
||||||
import { EodLeaderboard } from "./EodLeaderboard";
|
import { EodLeaderboard } from "./EodLeaderboard";
|
||||||
import { MinesweeperBoard } from "./MinesweeperBoard";
|
import { MinesweeperBoard } from "./MinesweeperBoard";
|
||||||
|
|
||||||
|
|
@ -25,6 +27,17 @@ export function EodArena({
|
||||||
setPlaying(phase === "playing");
|
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(() => {
|
useEffect(() => {
|
||||||
if (playing && isMobile) setOpenMobile(false);
|
if (playing && isMobile) setOpenMobile(false);
|
||||||
}, [isMobile, playing, setOpenMobile]);
|
}, [isMobile, playing, setOpenMobile]);
|
||||||
|
|
@ -38,6 +51,13 @@ export function EodArena({
|
||||||
<Bomb className="size-4" />
|
<Bomb className="size-4" />
|
||||||
Custom minefields
|
Custom minefields
|
||||||
</Link>
|
</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
|
<MinesweeperBoard
|
||||||
initialStats={initialStats}
|
initialStats={initialStats}
|
||||||
onGameRecorded={() => setRefreshKey((key) => key + 1)}
|
onGameRecorded={() => setRefreshKey((key) => key + 1)}
|
||||||
|
|
|
||||||
26
src/components/frontend/minigames/UnlockNotice.tsx
Normal file
26
src/components/frontend/minigames/UnlockNotice.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,13 @@ import { useCallback, useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Radio } from "lucide-react";
|
import { Radio } from "lucide-react";
|
||||||
import type { RadioStats } from "@/app/(frontend)/radio/actions";
|
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 { useRealtimePresence } from "@/hooks/useRealtimePresence";
|
||||||
|
import { UnlockNotice } from "@/components/frontend/minigames/UnlockNotice";
|
||||||
import { RadioLeaderboard } from "./RadioLeaderboard";
|
import { RadioLeaderboard } from "./RadioLeaderboard";
|
||||||
import { RadioTraffic } from "./RadioTraffic";
|
import { RadioTraffic } from "./RadioTraffic";
|
||||||
|
|
||||||
|
|
@ -24,6 +30,13 @@ export function RadioArena({
|
||||||
setPlaying(phase === "playing");
|
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(() => {
|
useEffect(() => {
|
||||||
const savedMode = window.localStorage.getItem("radio-traffic-mode");
|
const savedMode = window.localStorage.getItem("radio-traffic-mode");
|
||||||
if (savedMode === "sprint" || savedMode === "watch") setMode(savedMode);
|
if (savedMode === "sprint" || savedMode === "watch") setMode(savedMode);
|
||||||
|
|
@ -43,6 +56,13 @@ export function RadioArena({
|
||||||
<Radio className="size-4" />
|
<Radio className="size-4" />
|
||||||
Custom radio tests
|
Custom radio tests
|
||||||
</Link>
|
</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
|
<RadioTraffic
|
||||||
initialStats={initialStats}
|
initialStats={initialStats}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue