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

145 lines
6.0 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";
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",
};
/** 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 (
<Screen className="max-w-4xl">
<ScreenHeader title="Locations" eyebrow="Known space" icon="places" />
{lease.isError && (
<p role="alert" className="text-sm text-rose-300">
{errorMessage(lease.error)}
</p>
)}
<ul className="grid gap-3 md: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 tracking-wide 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 tracking-wide">
{location.name}
</span>
{hasBay && (
<Pill tone="emerald" icon="hq">
HQ
</Pill>
)}
</div>
<span className="font-display text-xs tracking-wider text-cyan-300/80">
{location.id === "earth"
? "Home"
: `${formatMillionKm(distanceAt("earth", location.id, now))} from Earth`}
</span>
</div>
</div>
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-space-950/50 p-2.5 text-ink-200 ring-1 ring-space-700">
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
Type
</dt>
<dd>{TYPE_LABELS[location.type]}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
Landing
</dt>
<dd>{LANDING_LABELS[location.landingRequirement]}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
Produces
</dt>
<dd>{location.cargoClassMix.produces.map(cargoName).join(", ") || "—"}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
Demands
</dt>
<dd>{location.cargoClassMix.demands.map(cargoName).join(", ") || "—"}</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>
);
}