From 540690590c1f11aa1ea2cf09c5be40282be99796 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sun, 4 Oct 2026 12:36:15 -0400 Subject: [PATCH] Logbook: show deadheads as an empty flight with fuel, not +0 cr Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 5 +++++ apps/server/src/game/player-state.ts | 2 ++ apps/server/test/loan.test.ts | 1 + apps/server/test/reposition.test.ts | 8 +++++++ apps/web/src/components/AwaySummary.tsx | 22 ++++++++++++------ apps/web/test/away-goals.test.tsx | 2 ++ apps/web/test/away-summary.test.tsx | 30 +++++++++++++++++++++++++ apps/web/test/deal-modal.test.tsx | 2 ++ apps/web/test/hq.test.tsx | 2 ++ apps/web/test/rest-summary-hq.test.tsx | 2 ++ apps/web/test/tutorial-board.test.tsx | 2 ++ packages/schema/src/entities.ts | 4 ++++ packages/schema/test/schemas.test.ts | 2 ++ 13 files changed, 77 insertions(+), 7 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 1564afc..b1a339d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -889,3 +889,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **A band counts as flyable if some ship can carry its minimum**, and a fit job's mass is capped at the chosen ship's capacity; so a Freighter also gets work from a band whose top it can't carry. - **Fit destinations are checked at the board's start time**, like the contract's own distance. A route at the edge of a ship's range could drift out during the 10-minute period; dispatch validation still decides. - **An uncrewed ship counts with no skills**, as if crewed by a fresh hire, so buying a ship immediately shapes the next board. + +## Follow-up after Phase 10F: deadheads in the logbook + +- **Found while checking deadhead end to end** (user request): a deadhead's "While you were away" entry read "+0 cr +0 XP" in success green. `ResolutionSummary` gains `deadhead` (the dispatch had no contract) and `fuelCost` (paid at departure); the entry now reads "Deadhead · 230 cr fuel" with a flight icon. Tests: `away-summary.test.tsx`, `reposition.test.ts`, `loan.test.ts`. +- **The missing favicon (a harmless 404) is left for later**, by the user's choice; Phase 11's PWA icons are the natural place. diff --git a/apps/server/src/game/player-state.ts b/apps/server/src/game/player-state.ts index 7440060..9f7fd5a 100644 --- a/apps/server/src/game/player-state.ts +++ b/apps/server/src/game/player-state.ts @@ -107,6 +107,8 @@ export async function loadPlayerState( originId: d.record.originId, destinationId: d.record.destinationId, tutorial: d.tutorial, + deadhead: d.record.contract === null, + fuelCost: d.record.fuelCost, credits: d.resolution?.credits ?? 0, loanRepayment: d.resolution?.loanRepayment ?? 0, pilotXp: d.resolution?.pilotXp ?? 0, diff --git a/apps/server/test/loan.test.ts b/apps/server/test/loan.test.ts index ca789da..53a327b 100644 --- a/apps/server/test/loan.test.ts +++ b/apps/server/test/loan.test.ts @@ -49,6 +49,7 @@ describe("loan repayment on delivery", () => { loanBalance: 5000 - repayment, }); expect(state.summaries[0]).toMatchObject({ + deadhead: false, credits: 2000 - repayment, loanRepayment: repayment, }); diff --git a/apps/server/test/reposition.test.ts b/apps/server/test/reposition.test.ts index df37e98..6efed35 100644 --- a/apps/server/test/reposition.test.ts +++ b/apps/server/test/reposition.test.ts @@ -35,6 +35,14 @@ describe("deadhead dispatch", () => { expect(state.ships[0]).toMatchObject({ status: "idle", locationId: "moon" }); expect(state.pilots[0]).toMatchObject({ status: "idle", locationId: "moon", xp: 0 }); expect(state.company.credits).toBe(10_000 - dispatch.fuelCost); + // The logbook marks it as a deadhead with its fuel (no payout to show). + expect(state.summaries).toHaveLength(1); + expect(state.summaries[0]).toMatchObject({ + deadhead: true, + fuelCost: dispatch.fuelCost, + credits: 0, + pilotXp: 0, + }); }); it("refuses an undiscovered destination and changes nothing", async () => { diff --git a/apps/web/src/components/AwaySummary.tsx b/apps/web/src/components/AwaySummary.tsx index e35166e..d101bee 100644 --- a/apps/web/src/components/AwaySummary.tsx +++ b/apps/web/src/components/AwaySummary.tsx @@ -47,13 +47,21 @@ export function AwaySummary() { {locationName(s.originId)} → {locationName(s.destinationId)} -
- = 0 ? "text-emerald-300" : "text-rose-300"}> - {s.credits >= 0 ? "+" : ""} - {formatCredits(s.credits)} - - +{s.pilotXp} XP -
+ {s.deadhead ? ( + // An empty flight earns nothing: say what it was and what it cost. +
+ + Deadhead · {formatCredits(s.fuelCost)} fuel +
+ ) : ( +
+ = 0 ? "text-emerald-300" : "text-rose-300"}> + {s.credits >= 0 ? "+" : ""} + {formatCredits(s.credits)} + + +{s.pilotXp} XP +
+ )} {s.loanRepayment > 0 && (
{formatCredits(s.loanRepayment)} went to the lender diff --git a/apps/web/test/away-goals.test.tsx b/apps/web/test/away-goals.test.tsx index 580af6e..36debc5 100644 --- a/apps/web/test/away-goals.test.tsx +++ b/apps/web/test/away-goals.test.tsx @@ -32,6 +32,8 @@ function returningPlayer(acknowledged: boolean): PlayerState { destinationId: "moon", tutorial: false, credits: 538, + deadhead: false, + fuelCost: 0, loanRepayment: 230, pilotXp: 10, discoveredLocationId: null, diff --git a/apps/web/test/away-summary.test.tsx b/apps/web/test/away-summary.test.tsx index cbc28fd..fc026bc 100644 --- a/apps/web/test/away-summary.test.tsx +++ b/apps/web/test/away-summary.test.tsx @@ -24,6 +24,8 @@ const summaries: ResolutionSummary[] = [ destinationId: "mars", tutorial: false, credits: 51_200, + deadhead: false, + fuelCost: 0, loanRepayment: 0, pilotXp: 32, discoveredLocationId: "mars", @@ -39,6 +41,8 @@ const summaries: ResolutionSummary[] = [ destinationId: "earth", tutorial: true, credits: 240, + deadhead: false, + fuelCost: 0, loanRepayment: 0, pilotXp: 10, discoveredLocationId: null, @@ -49,6 +53,32 @@ const summaries: ResolutionSummary[] = [ ]; describe("while you were away", () => { + it("shows a deadhead as an empty flight with its fuel, not as a +0 payout", async () => { + const deadhead: ResolutionSummary = { + ...summaries[0]!, + dispatchId: "d3", + destinationId: "moon", + deadhead: true, + fuelCost: 230, + credits: 0, + pilotXp: 0, + discoveredLocationId: null, + }; + renderApp( + fakeApi({ + getPlayerState: () => + samplePlayerState({ tutorial: null, ships: [ship], summaries: [deadhead] }), + }), + ); + const dialog = await screen.findByRole("dialog", { name: "While you were away" }); + const entry = within(dialog).getAllByRole("listitem")[0]!; + expect(entry).toHaveTextContent("Dusty Mule"); + expect(entry).toHaveTextContent("Earth → Moon"); + expect(entry).toHaveTextContent("Deadhead · 230 cr fuel"); + expect(entry).not.toHaveTextContent("+0 cr"); + expect(entry).not.toHaveTextContent("XP"); + }); + it("lists each completed delivery, and dismissing acknowledges them and hides the dialog", async () => { let acknowledged = false; const api = fakeApi({ diff --git a/apps/web/test/deal-modal.test.tsx b/apps/web/test/deal-modal.test.tsx index ee5fd28..50b190f 100644 --- a/apps/web/test/deal-modal.test.tsx +++ b/apps/web/test/deal-modal.test.tsx @@ -82,6 +82,8 @@ describe("the deal", () => { destinationId: "earth", tutorial: true, credits: 96, + deadhead: false, + fuelCost: 0, loanRepayment: 0, pilotXp: 10, discoveredLocationId: null, diff --git a/apps/web/test/hq.test.tsx b/apps/web/test/hq.test.tsx index 18c97b7..69ca5aa 100644 --- a/apps/web/test/hq.test.tsx +++ b/apps/web/test/hq.test.tsx @@ -146,6 +146,8 @@ describe("loan on HQ", () => { destinationId: "moon", tutorial: false, credits: 538, + deadhead: false, + fuelCost: 0, loanRepayment: 230, pilotXp: 10, discoveredLocationId: null, diff --git a/apps/web/test/rest-summary-hq.test.tsx b/apps/web/test/rest-summary-hq.test.tsx index c6c1f70..648a53d 100644 --- a/apps/web/test/rest-summary-hq.test.tsx +++ b/apps/web/test/rest-summary-hq.test.tsx @@ -33,6 +33,8 @@ const summary = (extra: Partial): ResolutionSummary => ({ destinationId: "moon", tutorial: false, credits: 500, + deadhead: false, + fuelCost: 0, loanRepayment: 0, pilotXp: 10, discoveredLocationId: null, diff --git a/apps/web/test/tutorial-board.test.tsx b/apps/web/test/tutorial-board.test.tsx index b1325f3..a4ae0a7 100644 --- a/apps/web/test/tutorial-board.test.tsx +++ b/apps/web/test/tutorial-board.test.tsx @@ -149,6 +149,8 @@ describe("scripted delivery summary", () => { destinationId: "moon", tutorial: true, credits: 461, + deadhead: false, + fuelCost: 0, loanRepayment: 0, pilotXp: 10, discoveredLocationId: null, diff --git a/packages/schema/src/entities.ts b/packages/schema/src/entities.ts index 95ef86d..fe20412 100644 --- a/packages/schema/src/entities.ts +++ b/packages/schema/src/entities.ts @@ -126,6 +126,10 @@ export const resolutionSummarySchema = z.object({ originId: idSchema, destinationId: idSchema, tutorial: z.boolean(), + /** An empty reposition flight: no cargo, no pay (CR-002). */ + deadhead: z.boolean(), + /** Fuel paid at departure. */ + fuelCost: credits.nonnegative(), /** Credits added after pilot pay and loan repayment. */ credits, /** Credits the lender took from this payout (decision 0.5.32). */ diff --git a/packages/schema/test/schemas.test.ts b/packages/schema/test/schemas.test.ts index ba8f725..18350e2 100644 --- a/packages/schema/test/schemas.test.ts +++ b/packages/schema/test/schemas.test.ts @@ -114,6 +114,8 @@ const summary = { destinationId: "earth", tutorial: true, credits: 240, + deadhead: false, + fuelCost: 0, loanRepayment: 0, pilotXp: 10, discoveredLocationId: null,