import { content } from "@deadhead/content"; import type { Dispatch, Ship } from "@deadhead/schema"; import { dispatchProgress, realToGame, tripFuelCost, validateReposition, type RepositionFailure, } from "@deadhead/sim"; import { useState } from "react"; import { Link } from "react-router"; import { useClock } from "../clock/clock"; import { Countdown } from "../components/Countdown"; import { formatCredits, formatDuration, locationName, shipModelName } from "../format"; import { location, shipModel, tripRealMs } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; /** All ships, their pilots and flights; any idle ship can deadhead (Steps 9.6, 10B.8). */ export function Fleet() { const state = useLoadedPlayer(); const [deadheading, setDeadheading] = useState(null); return (

Fleet

{state.ships.length === 0 ? (

No ships yet —{" "} visit the Market .

) : ( )} {deadheading && setDeadheading(null)} />}
); } function ShipRow(props: { ship: Ship; dispatch: Dispatch | undefined; pilotName: string | null; onDeadhead: () => void; }) { const { ship, dispatch, pilotName } = props; const now = useClock((s) => s.now); const progress = dispatch ? Math.round(dispatchProgress(dispatch, now) * 100) : 0; return (
  • {ship.name} {shipModelName(ship.modelId)}
    {dispatch ? `${locationName(dispatch.originId)} → ${locationName(dispatch.destinationId)}` : `At ${locationName(ship.locationId ?? "")}`} {" · "} {pilotName ?? "No pilot"}
    {dispatch && (
    Arrives in
    )} {ship.status === "idle" && ship.locationId && ( Find work here )} {ship.status === "idle" && ( )}
  • ); } /** Why a deadhead destination is unavailable, in plain words. */ const DEADHEAD_REASONS: Partial> = { "cannot-land": "Can't land there", "out-of-range": "Out of range", "insufficient-credits": "Not enough credits for fuel", "pilot-resting": "Pilot is resting", "pilot-not-idle": "Pilot is flying", "pilot-not-assigned": "No pilot assigned", }; /** * Fly an idle ship empty to any other discovered location, paying fuel only * (CR-002). The sim decides which destinations are possible, as the server will. */ export function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => void }) { const state = useLoadedPlayer(); const now = useClock((s) => s.now); const reposition = useGameAction("createDispatch"); const model = shipModel(ship.modelId); const origin = ship.locationId!; const pilot = state.pilots.find((p) => p.assignedShipId === ship.id) ?? null; const discovered = state.locations.filter((l) => l.discovered).map((l) => l.id); const discoveredIds = new Set(discovered); const gameNow = realToGame(now, content.config); const options = discovered .filter((id) => id !== origin) .map((id) => { const check = validateReposition( { ship: { id: ship.id, modelId: ship.modelId, locationId: origin, inFlight: false }, pilot: { id: pilot?.id ?? "", isAvatar: pilot?.isAvatar ?? false, assignedShipId: pilot ? ship.id : null, inFlight: pilot?.status === "in-flight", skills: pilot?.skills ?? {}, restEndsAt: pilot?.restEndsAt ?? null, }, destinationId: id, discoveredIds, credits: state.company.credits, now, }, content, ); return { id, problem: check.ok ? null : (DEADHEAD_REASONS[check.reason] ?? check.reason), fuelCost: tripFuelCost(model, location(origin), location(id), gameNow), tripMs: tripRealMs(model, origin, id, 0, now), }; }); return (

    Deadhead {ship.name}

    Fly empty from {locationName(origin)} to another place you know. You pay fuel only.

      {options.map((option) => (
    • ))}
    {reposition.isError && (

    {errorMessage(reposition.error)}

    )}
    ); }