import { content } from "@deadhead/content"; import { PilotIndependence } from "../components/IndependentPilots"; import type { Pilot } from "@deadhead/schema"; import { useState } from "react"; import { MS_PER_HOUR } from "@deadhead/sim"; import { Countdown } from "../components/Countdown"; import { formatCredits, formatDuration, formatGameHours, locationName, shipModelName, } from "../format"; import { ShopRest } from "../components/ShopRest"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Icon, Meter, Panel, Pill, Screen, ScreenHeader } from "../ui"; /** XP needed for the pilot's current and next level, or null at max level. */ export function levelProgress(xp: number, level: number) { const thresholds = content.config.pilotLevelXpThresholds; const next = thresholds[level]; if (next === undefined) return null; const previous = level === 0 ? 0 : thresholds[level - 1]!; return { xp: xp - previous, needed: next - previous }; } /** Pilots: levels, skills, assignment, duty and rest; hire, assign, rest and spend skill points (Steps 9.7, 10D.6). */ export function Pilots() { const state = useLoadedPlayer(); return ( {!state.pilots.some((p) => !p.isAvatar) && (

No hired pilots yet. Hire one at a bay to crew another ship.

)}
    {state.pilots.map((pilot) => ( ))}
); } function HirePilot() { const state = useLoadedPlayer(); const hire = useGameAction("hirePilot"); const [bay, setBay] = useState(state.bays[0]?.locationId ?? ""); const cost = content.config.pilotHireCost; const hasBay = state.bays.some((b) => b.locationId === bay); return ( {state.bays.length === 0 ? (

Lease a bay to hire pilots there.

) : (
)} {hire.isError && (

{errorMessage(hire.error)}

)}
); } function PilotCard({ pilot }: { pilot: Pilot }) { const state = useLoadedPlayer(); const spend = useGameAction("spendSkillPoint"); const progress = levelProgress(pilot.xp, pilot.level); const ship = state.ships.find((s) => s.id === pilot.assignedShipId); const lockedToLoaner = pilot.isAvatar && state.tutorial !== null; const initials = pilot.name .split(" ") .map((w) => w[0]) .join("") .slice(0, 2); return (
{pilot.name} {pilot.isAvatar && (you)} Level {pilot.level}
{pilot.isVeteran && (

{content.veteranPilot.bio}

)}
{progress ? `${progress.xp} / ${progress.needed} XP to next level` : `${pilot.xp} XP · max level`}
{pilot.status === "in-flight" ? ( In flight ) : ( `At ${locationName(pilot.locationId ?? "")}` )} {" · "} {lockedToLoaner ? "Flying the company loaner" : ship ? `${ship.name} (${shipModelName(ship.modelId)})` : "No ship"}
0 ? "text-amber-300" : "text-ink-400"}`} > Skill points:{" "} {pilot.skillPointsAvailable}
    {content.skills.map((skill) => { const level = pilot.skills[skill.id] ?? 0; const canRaise = pilot.skillPointsAvailable > 0 && level < skill.maxLevel; return (
  • {skill.name} {level}/{skill.maxLevel} {canRaise && ( )}
  • ); })}
{spend.isError && (

{errorMessage(spend.error)}

)}
{!lockedToLoaner && pilot.assignedShipId === null && pilot.status === "idle" && ( )} ); } /** Real time a rest lasts: at least one contract board period (decision 0.5.46). */ export function restRealMs(): number { const { restGameHours, timeCompression } = content.config; return (restGameHours * MS_PER_HOUR) / timeCompression; } /** Duty used of the limit, or the rest countdown; and "Rest now" with a confirmation (CR-004). */ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) { const rest = useGameAction("restPilot"); const [confirming, setConfirming] = useState(false); // Waiting with the ship in the shop counts as rest (decision 0.5.135). if (pilot.restingInShopUntil !== null) { return (
); } if (pilot.resting && pilot.restEndsAt !== null) { return (
Resting · wakes in
); } const used = Math.min(pilot.dutyHours, pilot.dutyLimit); return (
Duty {formatGameHours(pilot.dutyHours)} of {formatGameHours(pilot.dutyLimit)}
= 0.85 ? "rose" : "amber"} size="sm" className="mt-1" /> {canRest && pilot.status === "idle" && !confirming && ( )} {confirming && (

{pilot.name} rests for {formatDuration(restRealMs())} and can't fly until then. Rest always lasts at least one board refresh, so they wake to new contracts.

)} {rest.isError && (

{errorMessage(rest.error)}

)}
); } /** Assign an unassigned pilot to an idle, uncrewed ship at the same location. */ function AssignShip({ pilot }: { pilot: Pilot }) { const state = useLoadedPlayer(); const assign = useGameAction("assignPilot"); const ships = state.ships.filter( (s) => s.status === "idle" && s.pilotId === null && s.locationId === pilot.locationId, ); const [shipId, setShipId] = useState(""); if (ships.length === 0) { return (

No uncrewed ships at {locationName(pilot.locationId ?? "")}.

); } return (
{assign.isError && (

{errorMessage(assign.error)}

)}
); }