import type { CommandName } from "@deadhead/engine"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { createEngineClient } from "./api/engine-client"; import { ApiProvider } from "./api/context"; import { CatchingUp } from "./components/PilotLogbook"; import { GameContext } from "./platform/context"; import { startResolutionLoop, type Game } from "./platform/game"; import { platformPresence, startPresence } from "./platform/presence"; import { PLAYER_STATE_KEY } from "./state/player"; import { Button } from "./ui"; declare global { interface Window { /** Test builds only: run engine commands from browser tests (CR-014). */ deadhead?: { call(name: CommandName, input: unknown): Promise }; } } type Status = "starting" | "here" | "elsewhere"; /** * Runs the game in this tab (decision 0.5.90): takes the tab lock, provides the * engine as the API, and keeps the resolution loop going. Another tab holding * the game gets "open in another tab" with "Use here". */ export function GameRoot({ game, children }: { game: Game; children: ReactNode }) { const queryClient = useQueryClient(); const api = useMemo(() => createEngineClient(game.engine), [game]); const [status, setStatus] = useState("starting"); // A slow catch-up of Independent pilots shows "Catching up on your fleet…" (CR-021). const [catchingUp, setCatchingUp] = useState(false); useEffect(() => { game.lock.onLost(() => setStatus("elsewhere")); let cancelled = false; void game.lock.acquire().then((held) => { if (!cancelled) setStatus(held ? "here" : "elsewhere"); }); return () => { cancelled = true; }; }, [game]); useEffect(() => { if (status !== "here") return; let cancelled = false; let stop = () => {}; // The save's last time seen, read before the loop moves it on: a cold start // counts as coming back from then (decision 0.5.162). void game.storage.load().then((loaded) => { if (cancelled) return; const savedAt = "save" in loaded ? loaded.save.lastTimeSeen : null; const stopPresence = startPresence(platformPresence(), () => game.clock.now(), savedAt); const loop = startResolutionLoop( game, () => { void queryClient.invalidateQueries({ queryKey: PLAYER_STATE_KEY }); void queryClient.invalidateQueries({ queryKey: ["board"] }); }, setCatchingUp, ); stop = () => { loop.stop(); stopPresence(); }; }); if (game.devTools) { window.deadhead = { call: (name, input) => game.engine.call(name, input as never) }; } return () => { cancelled = true; stop(); }; }, [status, game, queryClient]); const useHere = async () => { if (await game.lock.acquire({ takeOver: true })) { // Continue from whatever the other tab saved. game.engine.core.reload(); await queryClient.invalidateQueries(); setStatus("here"); } }; if (status === "starting") { return

Loading…

; } if (status === "elsewhere") { return (

Deadhead is open in another tab.

); } return ( {children} ); }