Step 10G.10: delivery summaries name the good, mass and shipper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 17:21:52 -04:00
parent e96cd6ab2b
commit 66425753c4
4 changed files with 38 additions and 4 deletions

View File

@ -1,7 +1,7 @@
import { content } from "@deadhead/content";
import { useState } from "react";
import { useNavigate } from "react-router";
import { formatCredits, locationName } from "../format";
import { formatCredits, locationName, toLocation } from "../format";
import { formatLocalTime } from "../screens/DispatchSheet";
import { useGameAction, useLoadedPlayer } from "../state/actions";
import { Button, Icon, Sheet } from "../ui";
@ -47,6 +47,12 @@ export function AwaySummary() {
{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">

View File

@ -38,6 +38,11 @@ export function locationName(id: string): string {
return content.location(id)?.name ?? id;
}
/** A location as a destination in a sentence: "to the Moon", "to Mars". */
export function toLocation(id: string): string {
return id === "moon" ? "to the Moon" : `to ${locationName(id)}`;
}
export function shipModelName(id: string): string {
return content.shipModel(id)?.name ?? id;
}

View File

@ -59,6 +59,27 @@ const summaries: ResolutionSummary[] = [
];
describe("while you were away", () => {
it("names what was delivered, how much, for whom and where (CR-007)", async () => {
const delivery: ResolutionSummary = {
...summaries[0]!,
destinationId: "moon",
goodId: "medical-supplies",
massTonnes: 6,
shipper: "Meridian Health",
discoveredLocationId: null,
};
renderApp(
fakeApi({
getPlayerState: () =>
samplePlayerState({ tutorial: null, ships: [ship], summaries: [delivery] }),
}),
);
const dialog = await screen.findByRole("dialog", { name: "While you were away" });
expect(within(dialog).getAllByRole("listitem")[0]).toHaveTextContent(
"Delivered 6 t Medical Supplies for Meridian Health to the Moon",
);
});
it("shows a deadhead as an empty flight with its fuel, not as a +0 payout", async () => {
const deadhead: ResolutionSummary = {
...summaries[0]!,
@ -66,9 +87,9 @@ describe("while you were away", () => {
destinationId: "moon",
deadhead: true,
fuelCost: 230,
goodId: "consumer-goods",
massTonnes: 12,
shipper: "Pacific Orbital Logistics",
goodId: null,
massTonnes: null,
shipper: null,
credits: 0,
pilotXp: 0,
discoveredLocationId: null,
@ -86,6 +107,7 @@ describe("while you were away", () => {
expect(entry).toHaveTextContent("Deadhead · 230 cr fuel");
expect(entry).not.toHaveTextContent("+0 cr");
expect(entry).not.toHaveTextContent("XP");
expect(entry).not.toHaveTextContent("Delivered");
});
it("lists each completed delivery, and dismissing acknowledges them and hides the dialog", async () => {

View File

@ -211,3 +211,4 @@ Spec: `memory-bank/change-requests/CR-007-cargo-goods.md` (supplied as "CR-005 /
- [x] **Step 10G.7 — Server: pass-through and validation.** No server code changes were needed beyond 10G.4's message and 10G.6's summary fields: boards come from the sim and dispatch validation returns the sim's `too-slow`. Tests (`contract-dispatch.test.ts`): a Priority job whose window is shorter than the Courier's trip is refused with `too-slow` and changes nothing; with a window the Courier makes, it dispatches and the stored contract keeps its Priority good; every generated Earth contract names a catalog good of its class and a shipper from Earth's list.
- [x] **Step 10G.8 — Client: contract card redesign.** New `components/Goods.tsx`: `goodName` (a Priority good may use its `priorityName`, e.g. Emergency Parts), `ClassChip` ("High-Value · T1", toned by class), `PriorityChip` ("Priority · within 5h" in game hours; tap switches to real time, hover shows both), `tooSlowForAll` (idle ships at the origin exist and none makes the window, via sim `contractTripGameHours`), `formatWindow`, `realTimeOf`. Location board cards, top to bottom: route and payout; the good as an `h3` headline with class, Priority, First contact, Too heavy and Too slow chips; route strip; shipper · mass · distance; requirement and expiry with Dispatch. Tutorial cards show the good's name instead of the class. Tests: `contract-card.test.tsx` (good, class/tier chip, shipper, mass, requirement; Priority chip in game hours and real time on tap with the Priority name; Too slow with only a Bulk Hauler idle); `location-boards.test.tsx` updated to the new card.
- [x] **Step 10G.9 — Client: Priority in the dispatch sheet.** `explainProblems` adds "Too slow — 7h vs 5h window" (sim `contractTripGameHours` vs the window) so a slow pair is disabled with the numbers; `PairOption.priority` carries each pair's trip and the window, and eligible pairs show "Priority: 2.1h trip, 1.4h to spare in the 3.5h window". The sheet's job details now start with the good and shipper ("Mail & Parcels for Pacific Orbital Logistics · Standard · 6 t …") and Priority jobs show the window chip. Test: `dispatch-priority.test.tsx` (Courier enabled with its margin, Bulk Hauler disabled with "Too slow — …", numbers from the sim).
- [x] **Step 10G.10 — Client: goods in summaries.** Each delivery in "While you were away" says "Delivered 6 t Medical Supplies for Meridian Health to the Moon" (from the summary's `goodId`, `massTonnes`, `shipper`; new `toLocation` gives "to the Moon" / "to Mars"); deadheads keep their fuel line. Test: `away-summary.test.tsx` (the seeded delivery line; a deadhead shows no "Delivered").