import { content } from "@deadhead/content"; 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 { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; /** 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 (

Pilots

{!state.pilots.some((p) => !p.isAvatar) && (

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

)}
); } 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; return (
  • {pilot.name} {pilot.isAvatar && (you)} Level {pilot.level}
    {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"}
    Skill points:{" "} {pilot.skillPointsAvailable}
    {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); 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)}
    {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)}

    )}
    ); }