deadhead-space-freight-tycoon/apps/web/src/screens/Locations.tsx

96 lines
4.1 KiB
TypeScript

import { content, type Location } from "@deadhead/content";
import { Link } from "react-router";
import { useClock } from "../clock/clock";
import { cargoName, formatCredits, formatMillionKm } from "../format";
import { distanceAt } from "../game";
import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
const TYPE_LABELS: Record<Location["type"], string> = {
"atmospheric-world": "Atmospheric world",
"airless-body": "Airless body",
"cloud-platform": "Cloud platform",
"belt-station": "Belt station",
};
/** What a bay is for (CR-002: bays are HQs, not access to contracts). */
export const HQ_EXPLANATION =
"Open an HQ here: hire pilots and take delivery of new ships at this location.";
const LANDING_LABELS = { vacuum: "Any ship", atmospheric: "Atmospheric", hardened: "Hardened" };
/** Every location; details, contracts and HQ leasing once discovered (Steps 9.9, 10B.9). */
export function Locations() {
const state = useLoadedPlayer();
const now = useClock((s) => s.now);
const lease = useGameAction("leaseBay");
return (
<section className="space-y-3 p-4">
<h1 className="text-xl font-semibold">Locations</h1>
{lease.isError && (
<p role="alert" className="text-sm text-red-400">
{errorMessage(lease.error)}
</p>
)}
<ul className="space-y-2">
{content.locations.map((location) => {
const discovered = state.locations.find((l) => l.id === location.id)?.discovered;
if (!discovered) {
return (
<li key={location.id} className="rounded-lg border border-slate-800 p-3 text-sm">
<div className="font-medium">{location.name}</div>
<div className="text-slate-400">Undiscovered — deliver here to unlock.</div>
</li>
);
}
const hasBay = state.bays.some((b) => b.locationId === location.id);
const affordable = state.company.credits >= location.bayLeasePrice;
return (
<li key={location.id} className="rounded-lg border border-slate-800 p-3 text-sm">
<div className="flex items-baseline justify-between gap-2">
<span className="font-medium">{location.name}</span>
<span className="text-slate-400">
{location.id === "earth"
? "Home"
: `${formatMillionKm(distanceAt("earth", location.id, now))} from Earth`}
</span>
</div>
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-slate-300">
<dt className="text-slate-400">Type</dt>
<dd>{TYPE_LABELS[location.type]}</dd>
<dt className="text-slate-400">Landing</dt>
<dd>{LANDING_LABELS[location.landingRequirement]}</dd>
<dt className="text-slate-400">Produces</dt>
<dd>{location.produces.map(cargoName).join(", ") || "—"}</dd>
<dt className="text-slate-400">Demands</dt>
<dd>{location.demands.map(cargoName).join(", ") || "—"}</dd>
</dl>
<Link
to={`/contracts?location=${location.id}`}
className="mt-2 inline-block font-medium text-amber-400 underline"
>
View contracts
</Link>
{hasBay ? (
<p className="mt-2 font-medium text-emerald-400">Your HQ is here</p>
) : (
<>
<p className="mt-2 text-slate-400">{HQ_EXPLANATION}</p>
<button
type="button"
aria-label={`Lease bay at ${location.name}`}
disabled={!affordable || lease.isPending}
onClick={() => lease.mutate({ locationId: location.id })}
className="mt-2 w-full rounded-md bg-amber-500 py-1.5 font-semibold text-slate-950 disabled:opacity-40"
>
Lease bay ({formatCredits(location.bayLeasePrice)})
</button>
</>
)}
</li>
);
})}
</ul>
</section>
);
}