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:
parent
76fbaba293
commit
25975db23f
63 changed files with 250 additions and 92 deletions
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)",
|
||||
|
|
@ -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({
|
||||
|
|
@ -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",
|
||||
|
|
@ -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";
|
||||
|
|
@ -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({
|
||||
|
|
@ -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;
|
||||
|
|
@ -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({
|
||||
|
|
@ -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
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
validateMessages,
|
||||
validateRadioScore,
|
||||
validateTestName,
|
||||
} from "@/lib/custom-minigames/radio";
|
||||
} from "@/lib/minigames/custom/radio";
|
||||
|
||||
interface ActionResult<T = undefined> {
|
||||
success: boolean;
|
||||
|
|
@ -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)",
|
||||
|
|
@ -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({
|
||||
|
|
@ -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"],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
51
src/components/frontend/minigames/UgcLinkButton.tsx
Normal file
51
src/components/frontend/minigames/UgcLinkButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
@ -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";
|
||||
6
test-results/.last-run.json
Normal file
6
test-results/.last-run.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"e5c04ca02ae11ff5ff00-7644b2c9657ee2dfb45d"
|
||||
]
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)}`;
|
||||
|
|
|
|||
|
|
@ -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)}`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue