import { content } from "@deadhead/content"; import type { PlayerState, Ship } from "@deadhead/schema"; import { emergencyFuel, fuelPriceAt, refuel } from "@deadhead/sim"; import { useState } from "react"; import { formatCredits, locationName } from "../format"; import { simShip } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Meter, Sheet } from "../ui"; /** Fuel in whole fu, e.g. "1,170 fu" (CR-015). */ export function formatFu(fu: number): string { return `${Math.round(fu).toLocaleString("en-US")} fu`; } /** Credits per fu at a location (decision 0.5.96). */ export function fuelPriceOf(locationId: string): number { return fuelPriceAt(content.location(locationId)!, content.config); } /** "16 cr/fu"; prices are whole credits in the base game but may not be. */ export function formatFuelPrice(price: number): string { return `${Number.isInteger(price) ? price : price.toFixed(1)} cr/fu`; } /** The location (or locations) selling the cheapest fuel (decision 0.5.96). */ export function cheapestFuelIds(): Set { const selling = content.locations.filter((l) => l.sellsFuel); const lowest = Math.min(...selling.map((l) => l.fuelPrice)); return new Set(selling.filter((l) => l.fuelPrice === lowest).map((l) => l.id)); } /** A location's fuel price; the cheapest in the system is highlighted (Step 15.7). */ export function FuelPrice({ locationId }: { locationId: string }) { const location = content.location(locationId); if (!location?.sellsFuel) return None sold; const cheapest = cheapestFuelIds().has(locationId); return ( {formatFuelPrice(fuelPriceOf(locationId))} {cheapest && " · cheapest"} ); } const POLICY_LABELS = { auto: "Auto top-up", manual: "Manual refuel" } as const; /** A ship's fuel: fu, percent and refuel policy (Step 15.7). */ export function FuelGauge({ ship }: { ship: Ship }) { const capacity = ship.stats.tankCapacityFu; const percent = capacity > 0 ? Math.round((ship.fuelFu / capacity) * 100) : 0; return (
{formatFu(ship.fuelFu)} · {percent}% {POLICY_LABELS[ship.refuelPolicy]}
); } /** * "Refuel here first", priced: how much fuel it buys and what it costs, with * the same sim rules the engine charges by; `affordable` is false when the * credits don't cover even what the leg needs. */ export function refuelQuote( ship: Ship, neededFu: number, credits: number, ): { request: { shipId: string; amountFu?: number }; boughtFu: number; cost: number; affordable: boolean; } { const request = refuelForLeg(ship, neededFu, credits); const space = ship.stats.tankCapacityFu - ship.fuelFu; const bought = refuel( { fuelFu: ship.fuelFu, avgPrice: ship.fuelAvgPrice }, ship.stats.tankCapacityFu, request.amountFu ?? space, fuelPriceOf(ship.locationId ?? ""), ); const boughtFu = bought.tank.fuelFu - ship.fuelFu; return { request, boughtFu, cost: bought.cost, affordable: bought.cost <= credits }; } /** * What "Refuel here first" buys (Step 15.7): a full tank when the credits * cover it, otherwise just enough for the leg. */ export function refuelForLeg( ship: Ship, neededFu: number, credits: number, ): { shipId: string; amountFu?: number } { const price = fuelPriceOf(ship.locationId ?? ""); const space = ship.stats.tankCapacityFu - ship.fuelFu; if (credits >= Math.round(space * price)) return { shipId: ship.id }; return { shipId: ship.id, amountFu: Math.max(0, neededFu - ship.fuelFu) }; } /** The emergency fuel offer for a ship, or null (decision 0.5.99). */ export function emergencyOffer(state: PlayerState, ship: Ship) { if (ship.status !== "idle" || !ship.locationId) return null; return emergencyFuel( { ship: simShip(ship), credits: state.company.credits, discoveredIds: new Set(state.locations.filter((l) => l.discovered).map((l) => l.id)), }, content, ); } /** A ship's fuel: refuel here (fill or an amount), refuel policy, and emergency fuel (Step 15.7). */ export function ShipFuel({ shipId, onClose }: { shipId: string; onClose: () => void }) { const state = useLoadedPlayer(); const refuel = useGameAction("refuelShip"); const policy = useGameAction("setRefuelPolicy"); const emergency = useGameAction("orderEmergencyFuel"); const [amount, setAmount] = useState(""); const ship = state.ships.find((s) => s.id === shipId); if (!ship) return null; const here = ship.status === "idle" && ship.locationId ? content.location(ship.locationId) : null; const price = here ? fuelPriceOf(here.id) : 0; const space = Math.max(0, ship.stats.tankCapacityFu - ship.fuelFu); const fillCost = Math.round(space * price); const offer = emergencyOffer(state, ship); const amountFu = Number(amount); const error = refuel.error ?? policy.error ?? emergency.error; return (

Average price paid: {formatFuelPrice(Math.round(ship.fuelAvgPrice * 10) / 10)}

{!here ? (

Refuel when the ship has landed.

) : !here.sellsFuel ? (

{here.name} sells no fuel.

) : (

Fuel at {here.name}: {formatFuelPrice(price)}

{ e.preventDefault(); if (amountFu > 0) refuel.mutate({ shipId: ship.id, amountFu }); }} >
)}
Refuel policy
{(["manual", "auto"] as const).map((value) => ( ))}

{ship.refuelPolicy === "auto" ? "Fills the tank on every landing, as far as your credits go." : "The default: never buys fuel on its own. Refuel here, or with Refuel here first when a job needs it."}

{offer && (

{ship.name} can't reach anywhere from {locationName(ship.locationId ?? "")} and you can't afford the fuel.

)} {error && (

{errorMessage(error)}

)}
); }