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:
parent
169dc5d4c5
commit
540690590c
|
|
@ -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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ export async function loadPlayerState(
|
||||||
originId: d.record.originId,
|
originId: d.record.originId,
|
||||||
destinationId: d.record.destinationId,
|
destinationId: d.record.destinationId,
|
||||||
tutorial: d.tutorial,
|
tutorial: d.tutorial,
|
||||||
|
deadhead: d.record.contract === null,
|
||||||
|
fuelCost: d.record.fuelCost,
|
||||||
credits: d.resolution?.credits ?? 0,
|
credits: d.resolution?.credits ?? 0,
|
||||||
loanRepayment: d.resolution?.loanRepayment ?? 0,
|
loanRepayment: d.resolution?.loanRepayment ?? 0,
|
||||||
pilotXp: d.resolution?.pilotXp ?? 0,
|
pilotXp: d.resolution?.pilotXp ?? 0,
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ describe("loan repayment on delivery", () => {
|
||||||
loanBalance: 5000 - repayment,
|
loanBalance: 5000 - repayment,
|
||||||
});
|
});
|
||||||
expect(state.summaries[0]).toMatchObject({
|
expect(state.summaries[0]).toMatchObject({
|
||||||
|
deadhead: false,
|
||||||
credits: 2000 - repayment,
|
credits: 2000 - repayment,
|
||||||
loanRepayment: repayment,
|
loanRepayment: repayment,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ describe("deadhead dispatch", () => {
|
||||||
expect(state.ships[0]).toMatchObject({ status: "idle", locationId: "moon" });
|
expect(state.ships[0]).toMatchObject({ status: "idle", locationId: "moon" });
|
||||||
expect(state.pilots[0]).toMatchObject({ status: "idle", locationId: "moon", xp: 0 });
|
expect(state.pilots[0]).toMatchObject({ status: "idle", locationId: "moon", xp: 0 });
|
||||||
expect(state.company.credits).toBe(10_000 - dispatch.fuelCost);
|
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 () => {
|
it("refuses an undiscovered destination and changes nothing", async () => {
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,21 @@ export function AwaySummary() {
|
||||||
{locationName(s.originId)} → {locationName(s.destinationId)}
|
{locationName(s.originId)} → {locationName(s.destinationId)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex gap-4 font-display text-lg font-bold tabular-nums">
|
{s.deadhead ? (
|
||||||
<span className={s.credits >= 0 ? "text-emerald-300" : "text-rose-300"}>
|
// An empty flight earns nothing: say what it was and what it cost.
|
||||||
{s.credits >= 0 ? "+" : ""}
|
<div className="mt-2 flex items-center gap-1.5 font-display text-ink-300">
|
||||||
{formatCredits(s.credits)}
|
<Icon name="send" className="size-4 shrink-0 text-cyan-300" />
|
||||||
</span>
|
Deadhead · {formatCredits(s.fuelCost)} fuel
|
||||||
<span className="text-violet-300">+{s.pilotXp} XP</span>
|
</div>
|
||||||
</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 && (
|
{s.loanRepayment > 0 && (
|
||||||
<div className="mt-1 text-ink-400">
|
<div className="mt-1 text-ink-400">
|
||||||
{formatCredits(s.loanRepayment)} went to the lender
|
{formatCredits(s.loanRepayment)} went to the lender
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ function returningPlayer(acknowledged: boolean): PlayerState {
|
||||||
destinationId: "moon",
|
destinationId: "moon",
|
||||||
tutorial: false,
|
tutorial: false,
|
||||||
credits: 538,
|
credits: 538,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 230,
|
loanRepayment: 230,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ const summaries: ResolutionSummary[] = [
|
||||||
destinationId: "mars",
|
destinationId: "mars",
|
||||||
tutorial: false,
|
tutorial: false,
|
||||||
credits: 51_200,
|
credits: 51_200,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 0,
|
loanRepayment: 0,
|
||||||
pilotXp: 32,
|
pilotXp: 32,
|
||||||
discoveredLocationId: "mars",
|
discoveredLocationId: "mars",
|
||||||
|
|
@ -39,6 +41,8 @@ const summaries: ResolutionSummary[] = [
|
||||||
destinationId: "earth",
|
destinationId: "earth",
|
||||||
tutorial: true,
|
tutorial: true,
|
||||||
credits: 240,
|
credits: 240,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 0,
|
loanRepayment: 0,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
@ -49,6 +53,32 @@ const summaries: ResolutionSummary[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("while you were away", () => {
|
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 () => {
|
it("lists each completed delivery, and dismissing acknowledges them and hides the dialog", async () => {
|
||||||
let acknowledged = false;
|
let acknowledged = false;
|
||||||
const api = fakeApi({
|
const api = fakeApi({
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ describe("the deal", () => {
|
||||||
destinationId: "earth",
|
destinationId: "earth",
|
||||||
tutorial: true,
|
tutorial: true,
|
||||||
credits: 96,
|
credits: 96,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 0,
|
loanRepayment: 0,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,8 @@ describe("loan on HQ", () => {
|
||||||
destinationId: "moon",
|
destinationId: "moon",
|
||||||
tutorial: false,
|
tutorial: false,
|
||||||
credits: 538,
|
credits: 538,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 230,
|
loanRepayment: 230,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ const summary = (extra: Partial<ResolutionSummary>): ResolutionSummary => ({
|
||||||
destinationId: "moon",
|
destinationId: "moon",
|
||||||
tutorial: false,
|
tutorial: false,
|
||||||
credits: 500,
|
credits: 500,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 0,
|
loanRepayment: 0,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,8 @@ describe("scripted delivery summary", () => {
|
||||||
destinationId: "moon",
|
destinationId: "moon",
|
||||||
tutorial: true,
|
tutorial: true,
|
||||||
credits: 461,
|
credits: 461,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 0,
|
loanRepayment: 0,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,10 @@ export const resolutionSummarySchema = z.object({
|
||||||
originId: idSchema,
|
originId: idSchema,
|
||||||
destinationId: idSchema,
|
destinationId: idSchema,
|
||||||
tutorial: z.boolean(),
|
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 added after pilot pay and loan repayment. */
|
||||||
credits,
|
credits,
|
||||||
/** Credits the lender took from this payout (decision 0.5.32). */
|
/** Credits the lender took from this payout (decision 0.5.32). */
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ const summary = {
|
||||||
destinationId: "earth",
|
destinationId: "earth",
|
||||||
tutorial: true,
|
tutorial: true,
|
||||||
credits: 240,
|
credits: 240,
|
||||||
|
deadhead: false,
|
||||||
|
fuelCost: 0,
|
||||||
loanRepayment: 0,
|
loanRepayment: 0,
|
||||||
pilotXp: 10,
|
pilotXp: 10,
|
||||||
discoveredLocationId: null,
|
discoveredLocationId: null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue