import { content } from "@deadhead/content"; import { FleetAsleep } from "../components/FleetSleep"; import { NeedsYou } from "../components/NeedsYou"; import { DispatchOfficePanel } from "../components/IndependentPilots"; import type { Dispatch, Pilot, PlayerState, Ship, TutorialState } from "@deadhead/schema"; import { dispatchProgress } from "@deadhead/sim"; import { Link } from "react-router"; import { useClock } from "../clock/clock"; import { Countdown } from "../components/Countdown"; import { formatFu, FuelGauge } from "../components/Fuel"; import { GoalsCard } from "../components/GoalsCard"; import { goodName, PriorityChip } from "../components/Goods"; import { LoanCard } from "../components/LoanCard"; import { RouteStrip } from "../components/RouteStrip"; import { formatGameHours, locationName, shipModelName } from "../format"; import { lowFuel } from "../game"; import { ShopRest } from "../components/ShopRest"; import { useLoadedPlayer } from "../state/actions"; import { Art, buttonClass, Eyebrow, Icon, Meter, Panel, Pill, Screen, ScreenHeader, StatTile, } from "../ui"; /** What the player needs to do next to reach the deal (decision 0.5.30). */ export function tutorialHint(tutorial: TutorialState, runsBeforeDeal: number): string { if (tutorial.dealAvailable) return "Halvorsen is waiting at Earth with an offer."; if (tutorial.runsCompleted < runsBeforeDeal) { return "Keep flying — the old man has an offer for you."; } // Enough runs, but the deal is made at Earth. return tutorial.runInFlight ? "Halvorsen will talk once you land at Earth." : "One more run: bring the ship back to Earth — Halvorsen is waiting there."; } /** The ship the player's own pilot flies (decision 0.5.119). */ export type PilotShip = | { kind: "loaner"; avatar: Pilot; locationId: string; dispatch: Dispatch | undefined } | { kind: "ship"; avatar: Pilot; ship: Ship; dispatch: Dispatch | undefined } | { kind: "none"; avatar: Pilot | undefined }; /** The avatar's assigned ship, or the company loaner during the tutorial. */ export function pilotShip(state: PlayerState): PilotShip { const avatar = state.pilots.find((p) => p.isAvatar); if (!avatar) return { kind: "none", avatar }; if (state.tutorial) { return { kind: "loaner", avatar, locationId: state.tutorial.loanerLocationId, dispatch: state.activeDispatches.find((d) => d.tutorial), }; } const ship = state.ships.find((s) => s.id === avatar.assignedShipId); if (!ship) return { kind: "none", avatar }; return { kind: "ship", avatar, ship, dispatch: state.activeDispatches.find((d) => d.id === ship.dispatchId), }; } /** The display name of the ship a dispatch flies (the loaner isn't in the ships list). */ function flightShipName(state: PlayerState, dispatch: Dispatch): string { if (dispatch.tutorial) return "Company loaner"; return state.ships.find((s) => s.id === dispatch.shipId)?.name ?? "A ship"; } /** The Dashboard ("Bridge"): your ship first, then the company (CR-020). */ export function Dashboard() { const state = useLoadedPlayer(); const now = useClock((s) => s.now); const idle = state.ships.filter((s) => s.status === "idle").length; const flying = state.ships.filter((s) => s.status === "in-flight").length; const runsBeforeDeal = content.config.tutorialRunsBeforeDeal; const onJobs = [...state.activeDispatches].sort((a, b) => a.arrivalAt - b.arrivalAt); // Resting pilots and who wakes first (CR-004). const resting = state.pilots .filter((p) => p.resting && p.restEndsAt !== null) .sort((a, b) => a.restEndsAt! - b.restEndsAt!); // Ships in the upgrade shop, soonest out first (CR-009). const inShop = state.ships .filter((s) => s.inShopUntil !== null && now < s.inShopUntil) .sort((a, b) => a.inShopUntil! - b.inShopUntil!); return ( {/* While the fleet sleeps, first thing on the Bridge (CR-024). */} {state.tutorial && ( Contract pilot

Flying for {content.tutorialCompanyName}

Company runs: {Math.min(state.tutorial.runsCompleted, runsBeforeDeal)} of{" "} {runsBeforeDeal}

{tutorialHint(state.tutorial, runsBeforeDeal)}

{!state.tutorial.runInFlight && !state.tutorial.dealAvailable && ( Fly a company run )}
)}

In flight

{flying} flying · {idle} idle
{onJobs.length === 0 ? (

Nothing in flight.

) : (
    {onJobs.map((d) => { const name = flightShipName(state, d); return (
  • {name}{" "} {locationName(d.originId)} → {locationName(d.destinationId)}
  • ); })}
)}
{!state.tutorial && resting.length > 0 && (

Pilots resting: {resting.length} {resting[0]!.name} wakes in

)} {inShop.length > 0 && (

In the shop: {inShop.length}

    {inShop.map((ship) => (
  • {ship.name} · {content.upgrade(ship.shop?.upgradeId ?? "")?.name} {ship.shop?.level}{" "} · ready in
  • ))}
)} {!state.tutorial && state.ships.length === 0 && (

No ships yet — buy one in the Shipyard.

)} {!state.tutorial && } {!state.tutorial && state.company.loanBalance > 0 && } {!state.tutorial && ( Open the Logbook )}
); } /** The player pilot's ship: status, trip, fuel and the pilot's duty (decision 0.5.119). */ function YourShip({ className = "" }: { className?: string }) { const state = useLoadedPlayer(); const now = useClock((s) => s.now); const mine = pilotShip(state); if (mine.kind === "none") { return ( Your ship

You're not flying a ship right now.

Assign yourself in Crew
); } const { avatar, dispatch } = mine; const loaner = mine.kind === "loaner"; const ship = loaner ? null : mine.ship; const modelId = loaner ? content.config.tutorialLoanerShipModelId : ship!.modelId; const sprite = content.shipModel(modelId)?.sprite; const name = loaner ? "Company loaner" : ship!.name; const hereId = loaner ? mine.locationId : ship!.locationId; const inShop = ship !== null && ship.inShopUntil !== null && now < ship.inShopUntil; const discovered = new Set(state.locations.filter((l) => l.discovered).map((l) => l.id)); const low = ship && !dispatch ? lowFuel(ship, discovered) : null; const contract = dispatch?.contract ?? null; return (
{sprite && (
)}
Your ship

{name}

{dispatch ? ( In flight ) : inShop ? ( In the shop ) : ( Idle )}

{shipModelName(modelId)}

{dispatch ? `${locationName(dispatch.originId)} → ${locationName(dispatch.destinationId)}` : `At ${locationName(hereId ?? "")}`}

{dispatch && (
{contract ? `${contract.massTonnes} t ${goodName(contract)} for ${contract.shipper}` : "Flying empty"} Arrives in
{contract?.priority && contract.deliveryWindowGameHours !== null && (
)}
)} {inShop && (

In the shop — ready in

)}
Fuel {ship ? (
{low && (

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

)}
) : (

Supplied by Halvorsen

)}
{ship && !dispatch && !inShop && hereId && (
Find work here Fleet
)}
); } /** The player pilot's duty, or their rest (CR-004). */ function PilotDuty({ pilot }: { pilot: Pilot }) { return (
{pilot.name} · Level {pilot.level} {pilot.restingInShopUntil !== null ? (

) : pilot.resting && pilot.restEndsAt !== null ? (

Resting · wakes in

) : ( <> = 0.85 ? "rose" : "amber"} size="sm" className="mt-2" />

Duty {formatGameHours(pilot.dutyHours)} of {formatGameHours(pilot.dutyLimit)}

)}
); } /** One pip per company run toward the deal. */ function RunPips({ done, total }: { done: number; total: number }) { return ( ); }