deadhead-space-freight-tycoon/apps/web/src/components/AwaySummary.tsx

154 lines
6.4 KiB
TypeScript

import { content } from "@deadhead/content";
import { useState } from "react";
import { useNavigate } from "react-router";
import { useClock } from "../clock/clock";
import { formatCredits, locationName, toLocation } from "../format";
import { formatLocalTime } from "../screens/DispatchSheet";
import { useGameAction, useLoadedPlayer } from "../state/actions";
import { Button, Icon, Sheet } from "../ui";
/** "While you were away": deliveries resolved since the player last looked (Step 9.2). */
export function AwaySummary() {
const state = useLoadedPlayer();
const acknowledge = useGameAction("acknowledgeSummaries");
// Hide as soon as the player dismisses; the refetch clears the list for real.
const [dismissed, setDismissed] = useState<ReadonlySet<string>>(new Set());
const navigate = useNavigate();
const now = useClock((s) => s.now);
const summaries = state.summaries.filter((s) => !dismissed.has(s.dispatchId));
// Upgrades finished and not yet seen: "out of the shop" (CR-009).
const outOfShop = state.ships.filter(
(s) => s.shop !== null && s.shop.until <= now && !dismissed.has(`ship:${s.id}`),
);
// Goals waiting to be claimed (Step 10A.17).
const readyGoals = state.goals
.filter((g) => g.status === "ready")
.map((g) => content.onboardingGoal(g.id))
.filter((g) => g !== undefined);
if (summaries.length === 0 && outOfShop.length === 0) return null;
const shipName = (shipId: string, tutorial: boolean) =>
tutorial ? "Company loaner" : (state.ships.find((s) => s.id === shipId)?.name ?? "A ship");
const dismiss = () => {
const ids = summaries.map((s) => s.dispatchId);
const shipIds = outOfShop.map((s) => s.id);
setDismissed(new Set([...dismissed, ...ids, ...shipIds.map((id) => `ship:${id}`)]));
acknowledge.mutate({ dispatchIds: ids, ...(shipIds.length > 0 ? { shipIds } : {}) });
};
return (
<Sheet titleId="away-title" eyebrow="Logbook" title="While you were away">
<ul className="mt-4 space-y-3">
{summaries.map((s) => (
<li
key={s.dispatchId}
className="rounded-xl border border-space-600 bg-space-950/50 p-3 text-sm"
>
<div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide">
{shipName(s.shipId, s.tutorial)}
</span>
<span className="text-ink-400">
{locationName(s.originId)} → {locationName(s.destinationId)}
</span>
</div>
{!s.deadhead && s.goodId && s.massTonnes !== null && (
<p className="mt-1 text-ink-200">
Delivered {s.massTonnes} t {content.good(s.goodId)?.name ?? s.goodId}
{s.shipper && ` for ${s.shipper}`} {toLocation(s.destinationId)}
</p>
)}
{s.deadhead ? (
// An empty flight earns nothing: say what it was and what it cost.
<div className="mt-2 flex items-center gap-1.5 font-display text-ink-300">
<Icon name="send" className="size-4 shrink-0 text-cyan-300" />
Deadhead · {formatCredits(s.fuelCost)} fuel
</div>
) : (
<div className="mt-2 flex gap-4 font-display text-lg font-bold tabular-nums">
<span className={s.credits >= 0 ? "text-emerald-300" : "text-rose-300"}>
{s.credits >= 0 ? "+" : ""}
{formatCredits(s.credits)}
</span>
<span className="text-violet-300">+{s.pilotXp} XP</span>
</div>
)}
{s.loanRepayment > 0 && (
<div className="mt-1 text-ink-400">
{formatCredits(s.loanRepayment)} went to the lender
</div>
)}
{s.scriptedId && (
<p className="mt-1 text-fuchsia-200 italic">
{
content.scriptedTutorialContracts.find((c) => c.id === s.scriptedId)
?.flavorMessage
}
</p>
)}
{s.discoveredLocationId && (
<div className="mt-2 flex items-center gap-1.5 font-display font-semibold text-amber-300">
<Icon name="star" className="size-4" />
Discovered {locationName(s.discoveredLocationId)}!
</div>
)}
{s.restEndsAt !== null && (
<div className="mt-1 flex items-center gap-1.5 text-sky-300">
<Icon name="rest" className="size-4 shrink-0" />
<span>
{state.pilots.find((p) => p.id === s.pilotId)?.name ?? "The pilot"} is resting
until {formatLocalTime(s.restEndsAt)}
</span>
</div>
)}
</li>
))}
{outOfShop.map((ship) => (
<li
key={`shop:${ship.id}`}
className="flex items-center gap-1.5 rounded-xl border border-amber-400/40 bg-amber-950/30 p-3 text-sm text-amber-200"
>
<Icon name="check" className="size-4 shrink-0" />
{ship.name} is out of the shop:{" "}
{content.upgrade(ship.shop!.upgradeId)?.name ?? ship.shop!.upgradeId} {ship.shop!.level}
</li>
))}
</ul>
{readyGoals.length > 0 && (
<div className="mt-3 rounded-xl border border-emerald-400/40 bg-emerald-950/40 p-3 text-sm shadow-glow-emerald">
<h3 className="flex items-center gap-1.5 font-display font-bold text-emerald-300">
<Icon name="target" className="size-4" />
Goals ready to claim
</h3>
<ul aria-label="Ready goals" className="mt-1 space-y-0.5">
{readyGoals.map((goal) => (
<li key={goal.id}>
{goal.title}{" "}
<span className="font-display text-amber-300/90">
+{formatCredits(goal.reward)}
</span>
</li>
))}
</ul>
<Button
variant="success"
size="sm"
onClick={() => {
dismiss();
void navigate({ pathname: "/", hash: "#goals" });
}}
className="mt-3"
>
Claim on HQ
</Button>
</div>
)}
<Button block onClick={dismiss} className="mt-4">
Got it
</Button>
</Sheet>
);
}