import { content, upgradeMaxLevel, type ShipStat, type Upgrade } from "@deadhead/content"; import type { Ship } from "@deadhead/schema"; import { effectiveStats, nextUpgradeLevel, upgradeCost, upgradeShopMs, validateUpgrade, type EffectiveStats, type UpgradeFailure, } from "@deadhead/sim"; import { useState } from "react"; import { useClock } from "../clock/clock"; import { formatCredits, formatDuration, formatMillionKm } from "../format"; import { shipModel } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Pill, Sheet } from "../ui"; /** Why an upgrade can't be started now, in plain words (decisions 0.5.64, 0.5.65). */ export const UPGRADE_REASONS: Record = { "tutorial-active": "Finish the company runs first", "not-upgradable": "Company ship", "ship-not-idle": "In flight", "ship-in-shop": "In the shop", "not-at-hq": "Not at an HQ", "upgrade-maxed": "Maxed", "insufficient-funds": "Can't afford", }; /** How each upgradable stat reads, with its unit. */ const STAT_LABELS: Record string }> = { cruiseSpeedKmPerGameHour: { label: "Speed", format: (s) => `${formatMillionKm(s.cruiseSpeedKmPerGameHour / 1e6)}/h`, }, cargoCapacityTonnes: { label: "Capacity", format: (s) => `${Math.round(s.cargoCapacityTonnes * 10) / 10} t`, }, fuelRangeMillionKm: { label: "Range", format: (s) => formatMillionKm(s.fuelRangeMillionKm) }, overheadGameHours: { label: "Takeoff/landing", format: (s) => `${s.overheadGameHours.toFixed(2)} h`, }, handlingGameHoursPerTonne: { label: "Handling", format: (s) => `${(s.handlingGameHoursPerTonne * 60).toFixed(2)} min/t`, }, }; /** "Speed 2.00M km/h → 2.20M km/h": a stat now and after the next level. */ export function upgradeEffect(ship: Ship, upgrade: Upgrade): string | null { const model = shipModel(ship.modelId); const next = nextUpgradeLevel(upgrade, model.id, ship.upgrades); if (next === null) return null; const { label, format } = STAT_LABELS[upgrade.stat]; const before = effectiveStats(model, ship.upgrades, content); const after = effectiveStats(model, { ...ship.upgrades, [upgrade.id]: next }, content); return `${label} ${format(before)} → ${format(after)}`; } /** The ship's detail view: its upgrades, the next level of each, and starting one (CR-009). */ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: () => void }) { const state = useLoadedPlayer(); // The live ship, so the sheet shows the new level as soon as state refreshes. const ship = state.ships.find((s) => s.id === shipId)!; const now = useClock((s) => s.now); const start = useGameAction("startUpgrade"); const [confirming, setConfirming] = useState(null); const model = shipModel(ship.modelId); const atHq = ship.locationId !== null && state.bays.some((b) => b.locationId === ship.locationId); return (

Upgrades

    {content.upgrades.map((upgrade) => { const level = ship.upgrades[upgrade.id] ?? 0; const next = nextUpgradeLevel(upgrade, model.id, ship.upgrades); const check = validateUpgrade( { ship: { id: ship.id, modelId: ship.modelId, locationId: ship.locationId ?? "", inFlight: ship.status === "in-flight", upgrades: ship.upgrades, inShopUntil: ship.inShopUntil, }, upgrade, atHq, tutorialActive: state.tutorial !== null, credits: state.company.credits, now, }, content, ); const max = upgradeMaxLevel(upgrade, model.id); return (
  • {upgrade.name} Level {level}/{max}
    {next !== null && (
    {upgradeEffect(ship, upgrade)}
    )} {next !== null && (
    {formatCredits(upgradeCost(model, next, content.config))} ·{" "} {formatDuration(upgradeShopMs(next, content.config))} in the shop
    )}
    {check.ok ? ( ) : ( {UPGRADE_REASONS[check.reason]} )}
    {confirming?.id === upgrade.id && check.ok && (

    {upgrade.name} {check.level} costs {formatCredits(check.cost)}. {ship.name}{" "} can't fly for {formatDuration(check.shopUntil - now)} while it's in the shop.

    )}
  • ); })}
{start.isError && (

{errorMessage(start.error)}

)}
); }