1
0
Fork 0

refactor(minigames): move routes, components, and libs under minigames parents

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-09 00:02:59 -04:00
parent 76fbaba293
commit 25975db23f
63 changed files with 250 additions and 92 deletions

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,7 @@ import { headers as nextHeaders } from "next/headers";
import { notFound } from "next/navigation";
import Link from "next/link";
import { ArrowLeft, Bomb } from "lucide-react";
import { CustomMinefieldArena } from "@/components/frontend/eod/CustomMinefieldArena";
import { CustomMinefieldArena } from "@/components/frontend/minigames/eod/CustomMinefieldArena";
export const metadata: Metadata = {
title: "Play Custom Minefield (Polaris Task Force)",
@ -51,7 +51,7 @@ export default async function CustomMinefieldPlayPage({
<div className="flex min-w-0 flex-col gap-6 p-5">
<div className="flex items-center gap-2">
<Link
href="/eod/custom"
href="/minigames/eod/custom"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-4" /> All custom minefields

View file

@ -6,7 +6,7 @@ import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
import { hasPermission } from "@/utils/access-control/hasPermission";
import { broadcastToUser } from "@/lib/realtime/bus";
import { creditEodXp } from "@/lib/custom-minigames/payouts";
import { creditEodXp } from "@/lib/minigames/custom/payouts";
import {
applyCreatorBank,
customPlayerXp,
@ -16,7 +16,7 @@ import {
validateMinefieldName,
validateMinefieldScore,
validateMinePositions,
} from "@/lib/custom-minigames/eod";
} from "@/lib/minigames/custom/eod";
interface ActionResult<T = undefined> {
success: boolean;

View file

@ -3,8 +3,8 @@ import config from "@payload-config";
import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import { Bomb } from "lucide-react";
import { CustomMinefieldBrowser } from "@/components/frontend/eod/CustomMinefieldBrowser";
import { isEodUnlocked } from "@/lib/custom-minigames/eod";
import { CustomMinefieldBrowser } from "@/components/frontend/minigames/eod/CustomMinefieldBrowser";
import { isEodUnlocked } from "@/lib/minigames/custom/eod";
export const metadata: Metadata = {
title: "Custom Minefields (Polaris Task Force)",

View file

@ -4,7 +4,7 @@ import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import { redirect } from "next/navigation";
import { Bomb } from "lucide-react";
import { EodArena } from "@/components/frontend/eod/EodArena";
import { EodArena } from "@/components/frontend/minigames/eod/EodArena";
export const metadata: Metadata = {
title: "Minefield Clearance (Polaris Task Force)",
@ -18,7 +18,7 @@ export default async function EodPage() {
const { user } = await payload.auth({ headers, canSetHeaders: false });
if (!user) {
redirect(`/login?returnTo=${encodeURIComponent("/eod")}`);
redirect(`/login?returnTo=${encodeURIComponent("/minigames/eod")}`);
}
const profileRes = await payload.find({

View file

@ -4,7 +4,7 @@ import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import { redirect } from "next/navigation";
import { Bird } from "lucide-react";
import { FlappyArena } from "@/components/frontend/flappy/FlappyArena";
import { FlappyArena } from "@/components/frontend/minigames/flappy/FlappyArena";
export const metadata: Metadata = {
title: "Flight Simulator: Polaris Task Force",

View file

@ -12,7 +12,7 @@ import {
isOpordDifficulty,
minOpordRunTimeMs,
type OpordDifficulty,
} from "@/lib/opord-recall";
} from "@/lib/minigames/opord-recall";
import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
import { broadcastToUser } from "@/lib/realtime/bus";

View file

@ -4,7 +4,7 @@ import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import { redirect } from "next/navigation";
import { ScrollText } from "lucide-react";
import { OpordArena } from "@/components/frontend/opord/OpordArena";
import { OpordArena } from "@/components/frontend/minigames/opord/OpordArena";
export const metadata: Metadata = {
title: "OPORD Recall (Polaris Task Force)",
@ -18,7 +18,7 @@ export default async function OpordPage() {
const { user } = await payload.auth({ headers, canSetHeaders: false });
if (!user) {
redirect(`/login?returnTo=${encodeURIComponent("/opord")}`);
redirect(`/login?returnTo=${encodeURIComponent("/minigames/opord")}`);
}
const profileRes = await payload.find({

View file

@ -14,7 +14,7 @@ import {
MAX_HITS,
MAX_DESPAWNED_TARGETS,
MAX_CIVILIAN_HITS,
} from "@/lib/aim-trainer";
} from "@/lib/minigames/aim-trainer";
interface ActionResult<T = undefined> {
success: boolean;

View file

@ -5,8 +5,8 @@ import { headers as nextHeaders } from "next/headers";
import { redirect } from "next/navigation";
import { Crosshair } from "lucide-react";
import type { AimTrainerStats } from "@/app/(frontend)/qualification/actions";
import { AimTrainerArena } from "@/components/frontend/qualification/AimTrainerArena";
import type { AimTrainerStats } from "@/app/(frontend)/minigames/qualification/actions";
import { AimTrainerArena } from "@/components/frontend/minigames/qualification/AimTrainerArena";
export const metadata: Metadata = {
title: "Qualification Course: Polaris Task Force",
@ -20,7 +20,7 @@ export default async function QualificationPage() {
const { user } = await payload.auth({ headers, canSetHeaders: false });
if (!user) {
redirect(`/login?returnTo=${encodeURIComponent("/qualification")}`);
redirect(`/login?returnTo=${encodeURIComponent("/minigames/qualification")}`);
}
const profileRes = await payload.find({

View file

@ -5,7 +5,7 @@ import { headers as nextHeaders } from "next/headers";
import { notFound } from "next/navigation";
import Link from "next/link";
import { ArrowLeft, Radio } from "lucide-react";
import { CustomRadioArena } from "@/components/frontend/radio/CustomRadioArena";
import { CustomRadioArena } from "@/components/frontend/minigames/radio/CustomRadioArena";
export const metadata: Metadata = {
title: "Play Custom Radio Test (Polaris Task Force)",
@ -45,7 +45,7 @@ export default async function CustomRadioTestPlayPage({
<div className="flex min-w-0 flex-col gap-6 p-5">
<div className="flex items-center gap-2">
<Link
href="/radio/custom"
href="/minigames/radio/custom"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-4" /> All custom radio tests

View file

@ -11,7 +11,7 @@ import {
validateMessages,
validateRadioScore,
validateTestName,
} from "@/lib/custom-minigames/radio";
} from "@/lib/minigames/custom/radio";
interface ActionResult<T = undefined> {
success: boolean;

View file

@ -3,8 +3,8 @@ import config from "@payload-config";
import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import { Radio } from "lucide-react";
import { CustomTestBrowser } from "@/components/frontend/radio/CustomTestBrowser";
import { isRadioUnlocked } from "@/lib/custom-minigames/radio";
import { CustomTestBrowser } from "@/components/frontend/minigames/radio/CustomTestBrowser";
import { isRadioUnlocked } from "@/lib/minigames/custom/radio";
export const metadata: Metadata = {
title: "Custom Radio Tests (Polaris Task Force)",

View file

@ -4,7 +4,7 @@ import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import { redirect } from "next/navigation";
import { Radio } from "lucide-react";
import { RadioArena } from "@/components/frontend/radio/RadioArena";
import { RadioArena } from "@/components/frontend/minigames/radio/RadioArena";
export const metadata: Metadata = {
title: "Radio Traffic (Polaris Task Force)",
@ -18,7 +18,7 @@ export default async function RadioPage() {
const { user } = await payload.auth({ headers, canSetHeaders: false });
if (!user) {
redirect(`/login?returnTo=${encodeURIComponent("/radio")}`);
redirect(`/login?returnTo=${encodeURIComponent("/minigames/radio")}`);
}
const profileRes = await payload.find({

View file

@ -224,35 +224,35 @@ export const navigationCommands: NavigationCommand[] = [
id: "nav:qualification-course",
label: "Qualification Course",
icon: Crosshair,
href: "/qualification",
href: "/minigames/qualification",
keywords: ["aim trainer", "minigame", "shooting", "practice"],
},
{
id: "nav:radio-traffic",
label: "Radio Traffic",
icon: Radio,
href: "/radio",
href: "/minigames/radio",
keywords: ["minigame", "comms", "messages", "training"],
},
{
id: "nav:flight-simulator",
label: "Flight Simulator",
icon: PlaneTakeoff,
href: "/flappy",
href: "/minigames/flappy",
keywords: ["minigame", "flappy", "fly", "training"],
},
{
id: "nav:minefield-clearance",
label: "Minefield Clearance",
icon: Bomb,
href: "/eod",
href: "/minigames/eod",
keywords: ["minigame", "eod", "mines", "training"],
},
{
id: "nav:opord-recall",
label: "OPORD Recall",
icon: ScrollText,
href: "/opord",
href: "/minigames/opord",
keywords: ["minigame", "briefing", "memory", "operations order", "training"],
},
{

View file

@ -144,31 +144,31 @@ export function NavUser({
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem asChild>
<Link href="/flappy">
<Link href="/minigames/flappy">
<Bird />
Flight Simulator
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/eod">
<Link href="/minigames/eod">
<Bomb />
Minefield Clearance
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/radio">
<Link href="/minigames/radio">
<Radio />
Radio Traffic
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/qualification">
<Link href="/minigames/qualification">
<Crosshair />
Qualification Course
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/opord">
<Link href="/minigames/opord">
<FileTextIcon />
OPORD Recall
</Link>

View file

@ -0,0 +1,51 @@
import Link from "next/link";
import { ExternalLink, Wrench } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
/**
* Fully-featured nav button for a minigame's custom (UGC) area: an eyebrow
* status line, a title, and a pitch subtitle. Neutral outline styling, and the
* eyebrow flips between the unlocked celebration and the locked state.
*/
export function UgcLinkButton({
href,
title,
subtitle,
unlocked,
className,
}: {
href: string;
title: string;
subtitle: string;
unlocked: boolean;
className?: string;
}) {
return (
<Button
asChild
variant="outline"
className={cn("relative h-auto w-full gap-4 whitespace-normal px-4 py-4 text-left", className)}
>
<Link href={href}>
<span
aria-hidden
className="pointer-events-none absolute inset-0 animate-pulse rounded-md border-2 border-white/60"
/>
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
<Wrench className="size-5" />
</span>
<span className="flex min-w-0 flex-col items-start gap-1">
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
{unlocked ? "You've unlocked" : "Locked"}
</span>
<span className="text-sm font-semibold text-foreground">{title}</span>
<span className="text-xs font-normal leading-relaxed text-muted-foreground">
{subtitle}
</span>
</span>
<ExternalLink className="ml-auto shrink-0 text-muted-foreground" />
</Link>
</Button>
);
}

View file

@ -16,7 +16,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { createMinefield } from "@/app/(frontend)/eod/custom/actions";
import { createMinefield } from "@/app/(frontend)/minigames/eod/custom/actions";
const MIN_SIZE = 9;
const MAX_SIZE = 500;

View file

@ -127,7 +127,7 @@ export function CustomMinefieldBrowser({
</span>
</div>
<Button asChild size="sm">
<Link href={`/eod/custom/${row.id}`}>
<Link href={`/minigames/eod/custom/${row.id}`}>
<Play /> Play
</Link>
</Button>

View file

@ -3,9 +3,9 @@
import { useCallback, useEffect, useRef, useState, type CSSProperties, type PointerEvent } from "react";
import { Bomb, CircleHelp, Flag, RotateCcw, ShieldCheck, Timer, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { submitCustomMinefieldScore } from "@/app/(frontend)/eod/custom/actions";
import { submitCustomMinefieldScore } from "@/app/(frontend)/minigames/eod/custom/actions";
import { useSessionExpired } from "@/hooks/useSessionExpired";
import { edgeMineIndices } from "@/lib/custom-minigames/eod";
import { edgeMineIndices } from "@/lib/minigames/custom/eod";
import { cn } from "@/lib/utils";
import { playClear, playExplosion, playFlag, playReveal } from "./sounds";

View file

@ -1,13 +1,12 @@
"use client";
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 type { EodDifficulty, EodStats } from "@/app/(frontend)/minigames/eod/actions";
import { isEodUnlocked } from "@/lib/minigames/custom/eod";
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
import { useSidebar } from "@/components/ui/sidebar";
import { UnlockNotice } from "@/components/frontend/minigames/UnlockNotice";
import { UgcLinkButton } from "@/components/frontend/minigames/UgcLinkButton";
import { EodLeaderboard } from "./EodLeaderboard";
import { MinesweeperBoard } from "./MinesweeperBoard";
@ -44,13 +43,13 @@ export function EodArena({
return (
<div className="flex flex-col items-center gap-6">
<Link
href="/eod/custom"
className="flex items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-1.5 text-sm text-amber-300 transition-colors hover:bg-amber-500/20"
>
<Bomb className="size-4" />
Custom minefields
</Link>
<UgcLinkButton
href="/minigames/eod/custom"
title="UGC Minefields"
subtitle="Play, compete for, and create custom minefields you can share with other unit members! You can even earn XP for playing maps, and as an author, every payout on your map nets you some XP, too."
unlocked={customUnlocked}
className="max-w-5xl"
/>
{!customUnlocked && (
<UnlockNotice
title="custom minefields"

View file

@ -10,7 +10,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import type { EodDifficulty } from "@/app/(frontend)/eod/actions";
import type { EodDifficulty } from "@/app/(frontend)/minigames/eod/actions";
import type { PresenceParticipant } from "@/hooks/useRealtimePresence";
interface ApiUser {

View file

@ -3,7 +3,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Switch } from "@/components/ui/switch";
import { updateMinefield } from "@/app/(frontend)/eod/custom/actions";
import { updateMinefield } from "@/app/(frontend)/minigames/eod/custom/actions";
export function ExposureRuleToggle({
minefieldId,

View file

@ -15,7 +15,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { submitEodResult, type EodDifficulty, type EodStats } from "@/app/(frontend)/eod/actions";
import { submitEodResult, type EodDifficulty, type EodStats } from "@/app/(frontend)/minigames/eod/actions";
import { PROFILE_UPDATE_EVENT } from "@/hooks/useGameTick";
import { useSessionExpired } from "@/hooks/useSessionExpired";
import { LoginLink } from "@/components/frontend/auth/LoginLink";
@ -662,7 +662,7 @@ export function MinesweeperBoard({
phase === "playing" && "pointer-events-none opacity-40",
)}
>
<div className="grid gap-2 md:grid-cols-3">
<div className={cn("grid gap-2", customUnlocked ? "md:grid-cols-4" : "md:grid-cols-3")}>
{DIFFICULTY_KEYS.map((key) => {
const option = DIFFICULTIES[key];
return (
@ -684,8 +684,26 @@ export function MinesweeperBoard({
</Button>
);
})}
{customUnlocked && (
<Button
key="custom"
type="button"
variant={difficulty === "custom" ? "default" : "outline"}
className="h-auto justify-start px-3 py-2 text-left"
disabled={submitting || expired}
aria-pressed={difficulty === "custom"}
onClick={() => reset("custom")}
>
<span className="flex flex-col items-start gap-0.5">
<span>Custom</span>
<span className="text-[10px] font-normal opacity-75">
{customConfig.rows}×{customConfig.cols} grid · {customConfig.mines} charges
</span>
</span>
</Button>
)}
</div>
{customUnlocked ? (
{customUnlocked && difficulty === "custom" ? (
<div className="rounded-lg border border-emerald-500/40 bg-emerald-500/5 p-3">
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
@ -792,11 +810,11 @@ export function MinesweeperBoard({
{CUSTOM_MAX_SIZE}×{CUSTOM_MAX_SIZE} field.
</p>
</div>
) : (
) : !customUnlocked ? (
<p className="text-xs text-muted-foreground">
Clear all three standard locations to unlock custom minefields.
</p>
)}
) : null}
</fieldset>
<div

View file

@ -1,7 +1,7 @@
"use client";
import { useCallback, useState } from "react";
import type { FlappyStats } from "@/app/(frontend)/flappy/actions";
import type { FlappyStats } from "@/app/(frontend)/minigames/flappy/actions";
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
import { FlappyBird } from "./FlappyBird";
import { FlappyLeaderboard } from "./FlappyLeaderboard";

View file

@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { type FlappyStats, submitFlappyScore } from "@/app/(frontend)/flappy/actions";
import { type FlappyStats, submitFlappyScore } from "@/app/(frontend)/minigames/flappy/actions";
import { PROFILE_UPDATE_EVENT } from "@/hooks/useGameTick";
import { useSessionExpired } from "@/hooks/useSessionExpired";
import { LoginLink } from "@/components/frontend/auth/LoginLink";

View file

@ -2,8 +2,8 @@
import { useCallback, useEffect, useState } from "react";
import { OPORD_DIFFICULTIES, type OpordDifficulty } from "@/lib/opord-recall";
import type { OpordStats } from "@/app/(frontend)/opord/actions";
import { OPORD_DIFFICULTIES, type OpordDifficulty } from "@/lib/minigames/opord-recall";
import type { OpordStats } from "@/app/(frontend)/minigames/opord/actions";
import { useRealtimePresence } from "@/hooks/useRealtimePresence";
import { useSidebar } from "@/components/ui/sidebar";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

View file

@ -15,9 +15,9 @@ import {
type OpordDifficulty,
type OpordQuestion,
type OpordScenario,
} from "@/lib/opord-recall";
import type { OpordStats } from "@/app/(frontend)/opord/actions";
import { submitOpordRecallResult } from "@/app/(frontend)/opord/actions";
} from "@/lib/minigames/opord-recall";
import type { OpordStats } from "@/app/(frontend)/minigames/opord/actions";
import { submitOpordRecallResult } from "@/app/(frontend)/minigames/opord/actions";
import { useSessionExpired } from "@/hooks/useSessionExpired";
import { PROFILE_UPDATE_EVENT } from "@/hooks/useGameTick";
import { Button } from "@/components/ui/button";

View file

@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { Trophy } from "lucide-react";
import type { OpordDifficulty } from "@/lib/opord-recall";
import type { OpordDifficulty } from "@/lib/minigames/opord-recall";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { PresenceParticipant } from "@/hooks/useRealtimePresence";

View file

@ -1,4 +1,4 @@
import type { OpordDifficulty } from "@/lib/opord-recall";
import type { OpordDifficulty } from "@/lib/minigames/opord-recall";
export type OpordLeaderboardFields = {
readonly label: string;

View file

@ -6,8 +6,8 @@ import {
AIM_TRAINER_DIFFICULTIES,
isAimTrainerDifficulty,
type AimTrainerDifficulty,
} from "@/lib/aim-trainer";
import type { AimTrainerStats } from "@/app/(frontend)/qualification/actions";
} 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";

View file

@ -3,7 +3,7 @@ import {
calculateAimTrainerScore,
COURSE_DURATION_MS,
type AimTrainerTarget,
} from "@/lib/aim-trainer";
} from "@/lib/minigames/aim-trainer";
export const AIM_TRAINER_CANVAS_WIDTH = 720;
export const AIM_TRAINER_CANVAS_HEIGHT = 480;

View file

@ -19,12 +19,12 @@ import {
stepAimTrainerTargets,
TORSO_POINTS,
type AimTrainerDifficulty,
} from "@/lib/aim-trainer";
} from "@/lib/minigames/aim-trainer";
import {
type AimTrainerResultInput,
type AimTrainerStats,
submitAimTrainerResult,
} from "@/app/(frontend)/qualification/actions";
} from "@/app/(frontend)/minigames/qualification/actions";
import { LoginLink } from "@/components/frontend/auth/LoginLink";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

View file

@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { Trophy } from "lucide-react";
import type { AimTrainerDifficulty } from "@/lib/aim-trainer";
import type { AimTrainerDifficulty } from "@/lib/minigames/aim-trainer";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { PresenceParticipant } from "@/hooks/useRealtimePresence";

View file

@ -1,4 +1,4 @@
import type { AimTrainerDifficulty } from "@/lib/aim-trainer";
import type { AimTrainerDifficulty } from "@/lib/minigames/aim-trainer";
export type AimTrainerLeaderboardFields = {
readonly label: string;

View file

@ -21,7 +21,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { createTest } from "@/app/(frontend)/radio/custom/actions";
import { createTest } from "@/app/(frontend)/minigames/radio/custom/actions";
const MAX_MESSAGES = 100;

View file

@ -123,7 +123,7 @@ export function CustomTestBrowser({
</span>
</div>
<Button asChild size="sm">
<Link href={`/radio/custom/${row.id}`}>
<Link href={`/minigames/radio/custom/${row.id}`}>
<Play /> Play
</Link>
</Button>

View file

@ -1,16 +1,15 @@
"use client";
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 type { RadioStats } from "@/app/(frontend)/minigames/radio/actions";
import {
isRadioUnlocked,
SPRINT_ACCURACY_REQUIREMENT,
WATCH_ACCURACY_REQUIREMENT,
} from "@/lib/custom-minigames/radio";
} 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";
@ -49,13 +48,13 @@ export function RadioArena({
return (
<div className="flex flex-col items-center gap-6">
<Link
href="/radio/custom"
className="flex items-center gap-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 py-1.5 text-sm text-emerald-300 transition-colors hover:bg-emerald-500/20"
>
<Radio className="size-4" />
Custom radio tests
</Link>
<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"

View file

@ -5,8 +5,8 @@ import type { FormEvent, KeyboardEvent } from "react";
import { Check, Clock3, Radio, Target, Volume2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { type RadioStats, submitRadioResult } from "@/app/(frontend)/radio/actions";
import { submitCustomTestScore } from "@/app/(frontend)/radio/custom/actions";
import { type RadioStats, submitRadioResult } from "@/app/(frontend)/minigames/radio/actions";
import { submitCustomTestScore } from "@/app/(frontend)/minigames/radio/custom/actions";
import { LoginLink } from "@/components/frontend/auth/LoginLink";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

View file

@ -0,0 +1,6 @@
{
"status": "failed",
"failedTests": [
"e5c04ca02ae11ff5ff00-7644b2c9657ee2dfb45d"
]
}

View file

@ -1,6 +1,6 @@
import { expect, test, type Page } from "@playwright/test";
import { formatOpordGrid, generateOpordScenario } from "../../src/lib/opord-recall";
import { formatOpordGrid, generateOpordScenario } from "../../src/lib/minigames/opord-recall";
const FIXED_SEED = 424242;
const QUIZ_OPTION_BUTTON = "button.font-mono";
@ -14,7 +14,7 @@ async function login(page: Page): Promise<void> {
}
async function playOpordRound(page: Page): Promise<void> {
await page.goto(`/opord?seed=${FIXED_SEED}`);
await page.goto(`/minigames/opord?seed=${FIXED_SEED}`);
await expect(page.getByRole("heading", { name: "OPORD Recall" })).toBeVisible();
await page.getByRole("button", { name: "Receive briefing" }).click();

View file

@ -29,7 +29,7 @@ import {
resolveAimTrainerTargetHit,
stepAimTrainerTarget,
stepAimTrainerTargets,
} from "@/lib/aim-trainer";
} from "@/lib/minigames/aim-trainer";
/** Deterministic LCG-style RNG so target creation is reproducible across runs. */
function seededRng(seed: number): () => number {

View file

@ -17,8 +17,8 @@ import {
validateMinefieldName,
validateMinefieldScore,
validateMinePositions,
} from "@/lib/custom-minigames/eod";
import { creditEodXp } from "@/lib/custom-minigames/payouts";
} from "@/lib/minigames/custom/eod";
import { creditEodXp } from "@/lib/minigames/custom/payouts";
let payload: Payload;
const RUN = `cmf-${Date.now().toString(36)}`;

View file

@ -11,7 +11,7 @@ import {
validateMessages,
validateRadioScore,
validateTestName,
} from "@/lib/custom-minigames/radio";
} from "@/lib/minigames/custom/radio";
let payload: Payload;
const RUN = `crt-${Date.now().toString(36)}`;