Logbook: show deadheads as an empty flight with fuel, not +0 cr

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 12:36:15 -04:00
parent 169dc5d4c5
commit 540690590c
13 changed files with 77 additions and 7 deletions

View File

@ -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.

View File

@ -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,

View File

@ -49,6 +49,7 @@ describe("loan repayment on delivery", () => {
loanBalance: 5000 - repayment,
});
expect(state.summaries[0]).toMatchObject({
deadhead: false,
credits: 2000 - repayment,
loanRepayment: repayment,
});

View File

@ -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 () => {

View File

@ -47,13 +47,21 @@ export function AwaySummary() {
{locationName(s.originId)} → {locationName(s.destinationId)}
</span>
</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.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

View File

@ -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,

View File

@ -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({

View File

@ -82,6 +82,8 @@ describe("the deal", () => {
destinationId: "earth",
tutorial: true,
credits: 96,
deadhead: false,
fuelCost: 0,
loanRepayment: 0,
pilotXp: 10,
discoveredLocationId: null,

View File

@ -146,6 +146,8 @@ describe("loan on HQ", () => {
destinationId: "moon",
tutorial: false,
credits: 538,
deadhead: false,
fuelCost: 0,
loanRepayment: 230,
pilotXp: 10,
discoveredLocationId: null,

View File

@ -33,6 +33,8 @@ const summary = (extra: Partial<ResolutionSummary>): ResolutionSummary => ({
destinationId: "moon",
tutorial: false,
credits: 500,
deadhead: false,
fuelCost: 0,
loanRepayment: 0,
pilotXp: 10,
discoveredLocationId: null,

View File

@ -149,6 +149,8 @@ describe("scripted delivery summary", () => {
destinationId: "moon",
tutorial: true,
credits: 461,
deadhead: false,
fuelCost: 0,
loanRepayment: 0,
pilotXp: 10,
discoveredLocationId: null,

View File

@ -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). */

View File

@ -114,6 +114,8 @@ const summary = {
destinationId: "earth",
tutorial: true,
credits: 240,
deadhead: false,
fuelCost: 0,
loanRepayment: 0,
pilotXp: 10,
discoveredLocationId: null,