"use client"; import { Username } from "./Username"; import { PlayerAvatar } from "./PixelArt"; import { useCallback, useEffect, useRef, useState } from "react"; import { Maximize2, Minimize2, MapPin, Map as MapIcon, Users, } from "lucide-react"; import { Brand } from "./Brand"; import { Panorama } from "./Panorama"; import { GuessMap, type Pin } from "./GuessMap"; import { ReportContent } from "./ReportContent"; import { useI18n } from "./I18nProvider"; import { api } from "@/lib/client-api"; import type { RoomView, RoomMedia } from "@/shared/multiplayer"; export function MultiplayerGame({ id, room, busy, action, remaining, onLeave, mediaPath, embedded = false, leaveLabel = "Leave room", }: { id: string; room: RoomView; busy: boolean; action: (type: string, payload?: object) => Promise; remaining: (deadline: string) => number; onLeave: () => void; mediaPath?: string; embedded?: boolean; leaveLabel?: string; }) { const { t, formatNumber } = useI18n(); const [media, setMedia] = useState(null); const [error, setError] = useState(""); const [pin, setPin] = useState(null); const [expanded, setExpanded] = useState(false); const [loaded, setLoaded] = useState({ panorama: false, map: false }); const [seconds, setSeconds] = useState(0); const [nextIn, setNextIn] = useState(0); const sendingReady = useRef(false); const me = room.players.find((player) => player.id === room.you); const turn = room.rounds[room.round]; const result = turn.results.find((result) => result.playerId === room.you); const owner = room.hostId === room.you; const submitted = me?.submitted ?? false; const deadline = turn.deadline; useEffect(() => { let disposed = false; api(mediaPath ?? `rooms/${id}/media`) .then((next) => { if (!disposed && next.round === room.round) setMedia(next); }) .catch((failure) => { if (!disposed) setError(failure.message); }); return () => { disposed = true; }; }, [id, room.round, mediaPath]); useEffect(() => { if ( room.status !== "loading" || !loaded.panorama || !loaded.map || me?.ready || sendingReady.current || busy || error ) return; sendingReady.current = true; void action("ready", { round: room.round }).finally(() => { sendingReady.current = false; }); }, [room.status, room.round, me?.ready, loaded, busy, error, action]); useEffect(() => { const tick = () => { setSeconds(deadline ? remaining(deadline) : 0); setNextIn(room.transitionAt ? remaining(room.transitionAt) : 0); }; tick(); const timer = setInterval(tick, 250); return () => clearInterval(timer); }, [deadline, room.transitionAt, remaining]); const panoramaReady = useCallback( () => setLoaded((previous) => ({ ...previous, panorama: true })), [], ); const mapReady = useCallback( () => setLoaded((previous) => ({ ...previous, map: true })), [], ); const mediaError = useCallback((message: string) => setError(message), []); const Root = embedded ? "section" : "main"; const roundScores = [...turn.results].sort((a, b) => b.score - a.score); return ( {media && ( )}
{t("ROUND")} {room.round + 1} / 5
{room.difficultySettings.timerVisible !== false && (
{t("TIME LEFT")} {room.status === "loading" ? t("Loading…") : `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`}
)}
{t("POINTS")} {formatNumber(me?.total ?? 0)}
{t("Players")} · {room.playerCount}
    {room.players.map((player) => (
  • {player.id === room.you && {t("(you)")}} {player.left ? t("Left room") : room.status === "loading" ? player.ready ? t("Ready") : t("Loading…") : player.submitted ? t("Guess submitted") : t("Guessing")} {formatNumber(player.total)}
  • ))}
{media && (
{t( room.difficultySettings.compass === false ? "PLACE YOUR GUESS" : "↑ N · PLACE YOUR GUESS", )}
{submitted ? t("Guess locked. Waiting for the other players.") : pin ? room.difficultySettings.coordinates !== false ? `X ${Math.round(pin.x)} · Z ${Math.round(pin.z)}` : t("Guess selected") : t("Click the map to drop a pin")}
)} {room.status === "playing" && submitted && (
{t("Guess locked. Waiting for the other players.")}
)} {room.status === "playing" && seconds === 0 && !submitted && (
{t("Time is up. Calculating round scores…")}
)} {(!media || !loaded.panorama || !loaded.map || room.status === "loading") && !error && room.status !== "results" && (

{t("Preparing your expedition.")}

{loaded.panorama && loaded.map ? t("Waiting for players to load the shared round…") : t("Loading panorama and surface map…")}

{room.status === "loading" && ( {t("Starting within {seconds}s", { seconds: nextIn })} )}
)} {error && (

{t("Connection interrupted.")}

{t(error)}

)} {room.status === "results" && !error && (
{t("ROUND")} {room.round + 1} {t("COMPLETE")}

{t("Round standings")}

{formatNumber(result?.score ?? 0)} / 5,000

{result?.timedOut ? t("The clock ran out. This round scores zero.") : t("{count} blocks from the actual location.", { count: formatNumber(Math.round(result?.distance ?? 0)), })}

    {roundScores.map((score) => (
  1. { room.players.find((player) => player.id === score.playerId) ?.name } {formatNumber(score.score)}
  2. ))}
{owner && ( )}

{t("Next round in {seconds}s", { seconds: nextIn })}

)} ); }