import type { Contract, PlayerState, Ship } from "@deadhead/schema"; import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; import { useSearchParams } from "react-router"; import { useApi } from "../api/context"; import { serverNow, useClock } from "../clock/clock"; import { Countdown } from "../components/Countdown"; import { cargoName, formatClock, formatCredits, formatMillionKm, locationName, skillName, } from "../format"; import { distanceAt } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { pairOptions } from "./DispatchSheet"; import { DeadheadPicker } from "./Fleet"; import { content } from "@deadhead/content"; import { RouteStrip } from "../components/RouteStrip"; import { Art, Button, Icon, Panel, Pill } from "../ui"; export const locationBoardKey = (locationId: string) => ["board", "location", locationId] as const; /** Skills a contract needs, e.g. "Heavy Cargo 2, Long Haul 1", or null. */ export function requirementsText(contract: Contract): string | null { const parts = [ contract.requiredSkill ? `${skillName(contract.requiredSkill.skillId)} ${contract.requiredSkill.level}` : null, contract.requiredLongHaulLevel > 0 ? `Long Haul ${contract.requiredLongHaulLevel}` : null, ].filter((p): p is string => p !== null); return parts.length > 0 ? parts.join(", ") : null; } /** * The location the Contracts screen opens on: where the most recently arrived * idle ship is, otherwise Earth (CR-002). */ export function defaultBoardLocation(state: PlayerState): string { const idle = state.ships.filter((s) => s.status === "idle" && s.locationId); idle.sort((a, b) => b.lastArrivalAt - a.lastArrivalAt); return idle[0]?.locationId ?? "earth"; } /** Freight markets of every discovered location, no bay needed (Steps 9.4, 10B.7). */ export function LocationBoards({ onSelect }: { onSelect?: (contract: Contract) => void }) { const state = useLoadedPlayer(); // `?location=moon` (from the map or Fleet) selects that location. const [params, setParams] = useSearchParams(); const discovered = state.locations.filter((l) => l.discovered).map((l) => l.id); const requested = params.get("location"); const locationId = requested && discovered.includes(requested) ? requested : defaultBoardLocation(state); const idleAt = (id: string) => state.ships.filter((s) => s.status === "idle" && s.locationId === id).length; return (
{discovered.map((id) => { const idle = idleAt(id); const sprite = content.location(id)?.sprite; return ( ); })}
{idleAt(locationId) === 0 && (

No idle ships here. Deadhead one in from the Fleet screen to take these jobs.

)}
); } function LocationBoard({ locationId, onSelect, }: { locationId: string; onSelect?: (contract: Contract) => void; }) { const api = useApi(); const now = useClock((s) => s.now); const board = useQuery({ queryKey: locationBoardKey(locationId), queryFn: () => api.call("getContractBoard", { locationId }), // Fetch the new board as soon as the period rolls over (CR-004). refetchInterval: (query) => { const endsAt = query.state.data?.periodEndsAt; return endsAt === undefined ? false : Math.max(1000, endsAt - serverNow() + 500); }, }); if (board.isPending) return

Loading contracts…

; if (board.isError) { return (
Could not load contracts.{" "}
); } const refresh = (

New contracts in{" "}

); const stuck = ; if (board.data.contracts.length === 0) { return (
{refresh} {stuck} No contracts left this period.
); } return (
{refresh} {stuck}
); } /** * Idle, crewed ships here that cannot take any contract on this board (CR-004): * offer to rest the pilot, who wakes to a new board, or to deadhead elsewhere. */ function NothingHere({ locationId, contracts, now, }: { locationId: string; contracts: Contract[]; now: number; }) { const state = useLoadedPlayer(); const rest = useGameAction("restPilot"); const [deadheading, setDeadheading] = useState(null); const stuck = state.ships.filter((ship) => { const pilot = state.pilots.find((p) => p.assignedShipId === ship.id); if (ship.status !== "idle" || ship.locationId !== locationId || !pilot || pilot.resting) { return false; } return !contracts.some((c) => pairOptions(state, c, now).some((o) => o.shipId === ship.id && o.problems.length === 0), ); }); if (stuck.length === 0) return null; return ( <> {stuck.map((ship) => { const pilot = state.pilots.find((p) => p.assignedShipId === ship.id)!; return (

No job here that {ship.name} and {pilot.name} can take.

); })} {rest.isError && (

{errorMessage(rest.error)}

)} {deadheading && setDeadheading(null)} />} ); }