import { content } from "@deadhead/content"; import type { Pilot, PilotOrders, PlayerState } from "@deadhead/schema"; import { useState } from "react"; import { Link } from "react-router"; import { formatCredits, locationName } from "../format"; import { formatLocalTime } from "../screens/DispatchSheet"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Icon, Panel, Pill } from "../ui"; import { goodName } from "./Goods"; import { serviceStatus } from "./Service"; const inputClass = "rounded-lg border border-space-600 bg-space-950/70 px-3 py-2 text-sm text-ink-50 focus:border-amber-400 focus:outline-none"; /** The one-time Dispatch Office on the Bridge (decision 0.5.123). */ export function DispatchOfficePanel() { const state = useLoadedPlayer(); const buy = useGameAction("buyDispatchOffice"); if (state.tutorial) return null; const price = content.config.independentPilots.dispatchOfficePrice; if (state.dispatchOffice.owned) { return (

Dispatch Office

Your hired pilots can run jobs on their own. Set them to Independent on{" "} Pilots .

); } return (

Dispatch Office

Lets your hired pilots run jobs on their own, so the company keeps earning while you're away.

{buy.isError && (

{errorMessage(buy.error)}

)}
); } /** * What an Independent pilot is doing (CR-021 §2): flying a job, resting, or * waiting for new contracts; null for a Manual pilot. */ export function independentStatus(state: PlayerState, pilot: Pilot): string | null { if (pilot.mode !== "independent") return null; // Booked service comes first (CR-027 §4): heading in, in the shop, waiting for funds. const ship = state.ships.find((s) => s.id === pilot.assignedShipId); const service = ship && ship.booking?.status !== "cant-reach" ? serviceStatus(ship, pilot) : null; if (service) return service; const flight = state.activeDispatches.find((d) => d.pilotId === pilot.id); if (flight) { const where = locationName(flight.destinationId); return flight.contract ? `Independent: flying ${goodName(flight.contract)} to ${where}` : `Independent: flying empty to ${where}`; } if (pilot.restingInShopUntil !== null) { return `Resting while in the shop until ${formatLocalTime(pilot.restingInShopUntil)}`; } if (pilot.resting && pilot.restEndsAt !== null) { return `Resting until ${formatLocalTime(pilot.restEndsAt)}`; } if (!pilot.assignedShipId) return "Independent: needs a ship"; // The fleet has gone quiet: nothing new until the player decides (decision 0.5.137). if (state.fleetSleep.asleep) return "Waiting for orders"; return `Waiting for new contracts at ${locationName(pilot.locationId ?? "")}`; } /** Manual or Independent, the status line, and the orders (decisions 0.5.122–0.5.124). */ export function PilotIndependence({ pilot }: { pilot: Pilot }) { const state = useLoadedPlayer(); const setMode = useGameAction("setPilotMode"); // The player is an owner-operator: never Independent (decision 0.5.122). if (pilot.isAvatar || state.tutorial) return null; const owned = state.dispatchOffice.owned; const status = independentStatus(state, pilot); return (
Flies
{(["manual", "independent"] as const).map((mode) => ( ))}
{!owned && (

Buy a Dispatch Office on the Bridge to let {pilot.name} run jobs on their own.

)}
{status && (

{status}

)} {setMode.isError && (

{errorMessage(setMode.error)}

)} {owned && }
); } /** A pilot's orders (decision 0.5.124), edited as a draft and saved together. */ function OrdersEditor({ pilot }: { pilot: Pilot }) { const state = useLoadedPlayer(); const save = useGameAction("setPilotOrders"); const [draft, setDraft] = useState(pilot.orders); const discovered = state.locations.filter((l) => l.discovered).map((l) => l.id); const allPlaces = draft.destinationIds === null; const toggle = (list: readonly string[], id: string, on: boolean) => on ? [...new Set([...list, id])] : list.filter((x) => x !== id); return (
Orders
{ e.preventDefault(); save.mutate({ pilotId: pilot.id, orders: draft }); }} >
Destinations {!allPlaces && (
{discovered.map((id) => ( ))}
)}
Cargo
{content.cargoTypes.map((c) => ( ))}
{save.isSuccess && !save.isPending && ( Saved )} {save.isError && (

{errorMessage(save.error)}

)}
); } /** Settings → Company: the credits pilots never spend the company below (decision 0.5.127). */ export function SpendingFloor() { const state = useLoadedPlayer(); const save = useGameAction("setSpendingFloor"); const [draft, setDraft] = useState(String(state.dispatchOffice.spendingFloor)); if (state.tutorial) return null; const value = Math.floor(Number(draft)); const valid = draft.trim() !== "" && Number.isFinite(value) && value >= 0; return (
{ e.preventDefault(); if (valid) save.mutate({ credits: value }); }} > {save.isError && (

{errorMessage(save.error)}

)}
); } /** The Fleet card's badge for a ship whose pilot flies on their own. */ export function IndependentBadge({ pilot }: { pilot: Pilot | undefined }) { if (pilot?.mode !== "independent") return null; return ( Independent ); }