140 lines
5.9 KiB
TypeScript
140 lines
5.9 KiB
TypeScript
import { PlacesViews } from "../components/FleetViews";
|
|
import { content, type Location } from "@deadhead/content";
|
|
import { FuelPrice } from "../components/Fuel";
|
|
import { Link } from "react-router";
|
|
import { cargoName, formatCredits, formatMillionKm } from "../format";
|
|
import { distanceAt } from "../game";
|
|
import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
|
|
import { Art, Button, buttonClass, Icon, Panel, Pill, Screen, ScreenHeader } from "../ui";
|
|
|
|
const TYPE_LABELS: Record<Location["type"], string> = {
|
|
"atmospheric-world": "Atmospheric world",
|
|
"airless-body": "Airless body",
|
|
"cloud-platform": "Cloud platform",
|
|
"belt-station": "Belt station",
|
|
"orbital-station": "Orbital station",
|
|
"surface-base": "Surface base",
|
|
};
|
|
/** 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 lease = useGameAction("leaseBay");
|
|
|
|
return (
|
|
<Screen>
|
|
<ScreenHeader title="Places" icon="places" />
|
|
<PlacesViews />
|
|
{lease.isError && (
|
|
<p role="alert" className="text-sm text-rose-300">
|
|
{errorMessage(lease.error)}
|
|
</p>
|
|
)}
|
|
<ul className="grid gap-3 @2xl:grid-cols-2">
|
|
{content.locations.map((location) => {
|
|
const discovered = state.locations.find((l) => l.id === location.id)?.discovered;
|
|
if (!discovered) {
|
|
return (
|
|
<Panel
|
|
as="li"
|
|
key={location.id}
|
|
tone="muted"
|
|
className="flex items-center gap-4 text-sm"
|
|
>
|
|
<div className="relative shrink-0">
|
|
<Art sprite={location.sprite} className="size-16 opacity-40 grayscale" />
|
|
<Icon
|
|
name="lock"
|
|
className="absolute top-1/2 left-1/2 size-6 -translate-1/2 text-ink-200"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="font-display text-lg font-bold text-ink-300">{location.name}</div>
|
|
<div className="text-ink-400">Undiscovered — deliver here to unlock.</div>
|
|
</div>
|
|
</Panel>
|
|
);
|
|
}
|
|
const hasBay = state.bays.some((b) => b.locationId === location.id);
|
|
const affordable = state.company.credits >= location.bayLeasePrice;
|
|
return (
|
|
<Panel
|
|
as="li"
|
|
key={location.id}
|
|
tone={hasBay ? "emerald" : "default"}
|
|
className="flex flex-col text-sm"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<Art
|
|
sprite={location.sprite}
|
|
className="size-20 shrink-0 drop-shadow-[0_0_14px_rgb(125_211_252/0.35)]"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<span className="font-display text-xl font-bold">{location.name}</span>
|
|
{hasBay && (
|
|
<Pill tone="emerald" icon="hq">
|
|
HQ
|
|
</Pill>
|
|
)}
|
|
</div>
|
|
<span className="font-display text-xs text-ink-300">
|
|
{location.id === "earth"
|
|
? "Home"
|
|
: `${formatMillionKm(distanceAt("earth", location.id))} from Earth`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-black/25 p-2.5 text-ink-200 ring-1 ring-glass-line">
|
|
<dt className="font-display text-xs text-ink-400">Type</dt>
|
|
<dd>{TYPE_LABELS[location.type]}</dd>
|
|
<dt className="font-display text-xs text-ink-400">Landing</dt>
|
|
<dd>{LANDING_LABELS[location.landingRequirement]}</dd>
|
|
<dt className="font-display text-xs text-ink-400">Produces</dt>
|
|
<dd>{location.cargoClassMix.produces.map(cargoName).join(", ") || "—"}</dd>
|
|
<dt className="font-display text-xs text-ink-400">Demands</dt>
|
|
<dd>{location.cargoClassMix.demands.map(cargoName).join(", ") || "—"}</dd>
|
|
<dt className="font-display text-xs text-ink-400">Fuel</dt>
|
|
<dd aria-label={`Fuel at ${location.name}`}>
|
|
<FuelPrice locationId={location.id} />
|
|
</dd>
|
|
</dl>
|
|
<Link
|
|
to={`/contracts?location=${location.id}`}
|
|
className={`mt-3 ${buttonClass("secondary", "sm")}`}
|
|
>
|
|
<Icon name="jobs" className="size-4" />
|
|
View contracts
|
|
</Link>
|
|
{hasBay ? (
|
|
<p className="mt-3 flex items-center gap-1.5 font-display font-semibold text-emerald-300">
|
|
<Icon name="check" className="size-4" />
|
|
Your HQ is here
|
|
</p>
|
|
) : (
|
|
<>
|
|
<p className="mt-3 text-ink-400">{HQ_EXPLANATION}</p>
|
|
<Button
|
|
aria-label={`Lease bay at ${location.name}`}
|
|
block
|
|
disabled={!affordable || lease.isPending}
|
|
onClick={() => lease.mutate({ locationId: location.id })}
|
|
className="mt-2"
|
|
>
|
|
Lease bay ({formatCredits(location.bayLeasePrice)})
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Panel>
|
|
);
|
|
})}
|
|
</ul>
|
|
</Screen>
|
|
);
|
|
}
|