"use client"; import { useI18n } from "@/components/I18nProvider"; import { useEffect, useRef, useState, type FormEvent } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowRight, Timer, ZoomIn, Users, Trophy } from "lucide-react"; import { difficulties, customDifficultySchema, type DifficultyPreset, type ScoringMode, difficultyRuleDefaults, } from "@/shared/protocol"; import { api } from "@/lib/client-api"; import { mergeDifficultyPresets } from "@/shared/difficulty-presets"; export { api } from "@/lib/client-api"; export function StartGame({ datasetId, hostName, }: { datasetId: string; hostName: string | null; }) { const { t } = useI18n(); const [difficulty, setDifficulty] = useState("normal"), [customPresets, setCustomPresets] = useState([]), [showDifficultyForm, setShowDifficultyForm] = useState(false), [difficultyName, setDifficultyName] = useState(""), [difficultySeconds, setDifficultySeconds] = useState(90), [difficultyZoom, setDifficultyZoom] = useState(true), [difficultyRules, setDifficultyRules] = useState<{ pan: boolean; compass: boolean; coordinates: boolean; mapZoom: boolean; scoring: ScoringMode; }>(difficultyRuleDefaults), [savingDifficulty, setSavingDifficulty] = useState(false), [mode, setMode] = useState<"solo" | "multiplayer">("solo"), [playerLimit, setPlayerLimit] = useState(4), [loading, setLoading] = useState(false), [error, setError] = useState(""); const router = useRouter(); const tournamentPath = `/tournaments?map=${encodeURIComponent(datasetId)}`; const starting = useRef(false); const savingPreset = useRef(false); useEffect(() => { let disposed = false; api<{ presets: DifficultyPreset[] }>("difficulty-presets") .then(({ presets }) => { if (!disposed) setCustomPresets((current) => mergeDifficultyPresets(current, presets), ); }) .catch((failure) => { if (!disposed) setError(failure.message); }); return () => { disposed = true; }; }, []); const options: DifficultyPreset[] = [ ...Object.entries(difficulties).map(([id, preset]) => ({ id, name: preset.label, seconds: preset.seconds, zoom: preset.zoom, })), ...customPresets, ]; async function saveDifficulty(event: FormEvent) { event.preventDefault(); if (savingPreset.current || starting.current) return; const parsed = customDifficultySchema.safeParse({ name: difficultyName, seconds: difficultySeconds, zoom: difficultyZoom, ...difficultyRules, }); if (!parsed.success) { setError(parsed.error.issues[0].message); return; } savingPreset.current = true; setSavingDifficulty(true); setError(""); try { const { preset } = await api<{ preset: DifficultyPreset }>( "difficulty-presets", parsed.data, ); setCustomPresets((current) => mergeDifficultyPresets(current, [preset])); setDifficulty(preset.id); setDifficultyName(""); setDifficultySeconds(90); setDifficultyZoom(true); setDifficultyRules(difficultyRuleDefaults); setShowDifficultyForm(false); } catch (failure) { setError((failure as Error).message); } finally { savingPreset.current = false; setSavingDifficulty(false); } } async function start() { if (starting.current || savingPreset.current) return; starting.current = true; setLoading(true); setError(""); try { const session = await api<{ id: string }>( mode === "solo" ? "sessions" : "rooms", { datasetId, difficulty, ...(mode === "multiplayer" ? { playerLimit } : {}), }, ); router.push((mode === "solo" ? "/play/" : "/multiplayer/") + session.id); } catch (e) { starting.current = false; setError((e as Error).message); setLoading(false); } } return (
{t("YOUR EXPEDITION")}

{t("Choose your challenge.")}

{t( hostName ? "Create tournament with this map" : "Log in to create a tournament", )}
{options.map((option) => ( ))}
{showDifficultyForm && (
setDifficultyName([...event.target.value].slice(0, 40).join("")) } minLength={2} maxLength={80} required autoComplete="off" placeholder={t("Name this difficulty")} disabled={savingDifficulty || loading} />
setDifficultySeconds(Number(event.target.value)) } required disabled={savingDifficulty || loading} /> {t("s per round")}
{( [ ["pan", "Allow panorama panning"], ["compass", "Show north indicator"], ["coordinates", "Show guess coordinates"], ["mapZoom", "Allow guess-map zoom"], ] as const ).map(([key, label]) => ( ))}

{t( "Strict scoring penalizes distance more; gentle scoring gives more credit for nearby guesses. Every game still has five rounds.", )}

)}

{t("5 rounds · 25,000 possible points · No movement")}

{mode === "multiplayer" && (
{hostName ? (

{t("Host display name")}: {hostName}

) : (

{t("Log in to create a multiplayer room")}{" "} {t("Log in")}

)}

{t( "Create a room, share the invite link, and start when your friends arrive.", )}

)} {error && (

{t(error)}

)}
); }