96 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|