import { content } from "@deadhead/content"; import { IndependentBadge } from "../components/IndependentPilots"; import type { Dispatch, Pilot, Ship } from "@deadhead/schema"; import { dispatchProgress, tripFuel, 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 { formatFu, FuelGauge, refuelForLeg, ShipFuel } from "../components/Fuel"; import { ShipUpgrades } from "./ShipUpgrades"; import { ServiceTag } from "../components/Service"; import { strandedText } from "./DispatchSheet"; import { formatClock, formatCredits, formatDuration, locationName, shipModelName } from "../format"; import { location, lowFuel, shipModel, tripRealMs, shipStatsOf, simShip } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { Art, Button, buttonClass, Icon, Meter, Panel, Pill, Screen, ScreenHeader, Sheet, } from "../ui"; /** 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); const [upgrading, setUpgrading] = useState(null); const [fueling, setFueling] = useState(null); const flying = state.ships.filter((s) => s.status === "in-flight").length; const discoveredIds = new Set(state.locations.filter((l) => l.discovered).map((l) => l.id)); return ( 0 ? `${state.ships.length} ${state.ships.length === 1 ? "ship" : "ships"} · ${flying} flying` : "Your ships" } icon="fleet" /> {state.ships.length === 0 ? (

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

) : (
    {state.ships.map((ship) => ( d.id === ship.dispatchId)} pilotName={state.pilots.find((p) => p.id === ship.pilotId)?.name ?? null} pilot={state.pilots.find((p) => p.id === ship.pilotId)} discoveredIds={discoveredIds} onDeadhead={() => setDeadheading(ship)} onUpgrades={() => setUpgrading(ship.id)} onFuel={() => setFueling(ship.id)} /> ))}
)} {deadheading && setDeadheading(null)} />} {upgrading && setUpgrading(null)} />} {fueling && setFueling(null)} />}
); } function ShipRow(props: { ship: Ship; dispatch: Dispatch | undefined; pilotName: string | null; pilot: Pilot | undefined; discoveredIds: ReadonlySet; onDeadhead: () => void; onUpgrades: () => void; onFuel: () => void; }) { const { ship, dispatch, pilotName } = props; const now = useClock((s) => s.now); const progress = dispatch ? Math.round(dispatchProgress(dispatch, now) * 100) : 0; const model = shipModel(ship.modelId); // In the upgrade shop until then (CR-009); the clock decides, so it ends on time. const inShop = ship.inShopUntil !== null && now < ship.inShopUntil; // Ships no longer refuel on their own (CR-018): say when the tank is short. const low = dispatch ? null : lowFuel(ship, props.discoveredIds); return (
{ship.name} {dispatch ? ( In flight ) : inShop ? ( In the shop ) : ( Idle )}
{shipModelName(ship.modelId)}
{dispatch ? `${locationName(dispatch.originId)} → ${locationName(dispatch.destinationId)}` : `At ${locationName(ship.locationId ?? "")}`} {" · "} {pilotName ?? "No pilot"} {/* A pilot who flies on their own (CR-021). */}
{dispatch && (
{progress}% Arrives in{" "}
)} {low && (

Low fuel {locationName(low.destinationId)} needs {formatFu(low.fuelFu)}

)} {inShop && (

{ship.repairing ? "Under repair" : "In the shop"} —{" "}

)} {ship.status === "idle" && !inShop && (
{ship.locationId && ( Find work here )}
)}
); } /** 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", "would-strand": "Would be stranded", "not-enough-fuel": "Not enough fuel", "ship-in-shop": "Ship is in the shop", "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 refuel = useGameAction("refuelShip"); // Effective stats: upgraded fuel tanks reach farther (CR-009). const model = shipStatsOf(ship); 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 options = discovered .filter((id) => id !== origin) .map((id) => { const check = validateReposition( { ship: simShip(ship), 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, now, }, content, ); const neededFu = tripFuel(model, location(origin), location(id), content.config); return { id, neededFu, shortOfFuel: !check.ok && check.reason === "not-enough-fuel", problem: check.ok ? null : check.reason === "would-strand" ? strandedText(id) : (DEADHEAD_REASONS[check.reason] ?? check.reason), // The fuel used, at the tank's average price (decision 0.5.98). fuelCost: Math.round(neededFu * ship.fuelAvgPrice), tripMs: tripRealMs(model, origin, id, 0), }; }); // Enough for the hungriest leg that is only short of fuel (Step 15.7). const shortLegFu = Math.max(0, ...options.filter((o) => o.shortOfFuel).map((o) => o.neededFu)); return (

On board: {formatFu(ship.fuelFu)} of {formatFu(ship.stats.tankCapacityFu)}

{shortLegFu > 0 && ( )}
    {options.map((option) => { const sprite = content.location(option.id)?.sprite; return (
  • ); })}
{(reposition.isError || refuel.isError) && (

{errorMessage(reposition.error ?? refuel.error)}

)}
); } /** * A ship's hull condition (decision 0.5.148), with a red "Needs repair" below * 25%: repairs are done at an HQ from the ship's Upgrades sheet. */ export function HullGauge({ ship }: { ship: Ship }) { const pct = ship.hullConditionPct; const { needsRepairBelowPct, noDangerousBelowPct } = content.config.hull; const tone = pct < needsRepairBelowPct ? "rose" : pct < noDangerousBelowPct ? "amber" : "emerald"; return (
Hull {Math.round(pct)}%
{pct < needsRepairBelowPct && (

Needs repair Repair it at an HQ

)}
); }