154 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|