diff --git a/DECISIONS.md b/DECISIONS.md index 0ca6137..bc25f54 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1018,3 +1018,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **The veteran is a pilots row with `is_veteran`**, otherwise identical to a hire (pay, duty, rest, assignment rules). The flag only identifies them for goal 4 and the bio line. - **Level 2 is stored as XP**: the veteran starts with exactly level 2's threshold (250 XP), so levels and skill points come from the normal rules; one point is recorded as spent on Heavy Cargo 1. - **Created inside `acceptDeal`**, after the deal check, in the player-locked transaction: a refused or repeated accept creates nothing (tested). + +## Step 10J.5 + +- **Name and bio come from `content.veteranPilot`** in both places; the pilot card shows the bio when the API marks the pilot `isVeteran`. +- **The modal adds a "Pilot" row** to its "You get / Loan / Repayment" list, in the CR's "…and [name]" style, and its opening line mentions the pilot. diff --git a/apps/web/src/components/DealModal.tsx b/apps/web/src/components/DealModal.tsx index 38371b3..3ff6513 100644 --- a/apps/web/src/components/DealModal.tsx +++ b/apps/web/src/components/DealModal.tsx @@ -29,6 +29,7 @@ export function DealModal() { const { dealLoanAmount, loanRepaymentShare } = content.config; const courier = content.shipModel("courier")!; + const veteran = content.veteranPilot; return (

- Old Halvorsen is retiring. He'll sell you his Courier and his Earth bay. Pay him back - out of every job. + Old Halvorsen is retiring. He'll sell you his Courier and his Earth bay, and his + pilot comes with them. Pay him back out of every job.

@@ -50,6 +51,10 @@ export function DealModal() { You get
A {courier.name} and a bay at Earth
+
Pilot
+
+ …and {veteran.name}. {veteran.bio} +
Loan
+ {pilot.isVeteran && ( +

+ {content.veteranPilot.bio} +

+ )}
{progress ? `${progress.xp} / ${progress.needed} XP to next level` diff --git a/apps/web/test/veteran-ui.test.tsx b/apps/web/test/veteran-ui.test.tsx new file mode 100644 index 0000000..c5c430e --- /dev/null +++ b/apps/web/test/veteran-ui.test.tsx @@ -0,0 +1,62 @@ +import { content } from "@deadhead/content"; +import type { Pilot } from "@deadhead/schema"; +import { screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { fakeApi, renderApp, samplePlayerState, sampleTutorialBoard } from "./render"; + +const veteran = content.veteranPilot; + +describe("the veteran in the UI (Step 10J.5)", () => { + it("names the veteran with their bio in the deal modal", async () => { + renderApp( + fakeApi({ + getPlayerState: () => + samplePlayerState({ + tutorial: { + loanerLocationId: "earth", + runInFlight: false, + runsCompleted: 4, + dealAvailable: true, + }, + }), + getTutorialBoard: () => sampleTutorialBoard(), + }), + "/contracts", + ); + const modal = await screen.findByRole("dialog", { name: "An offer from Halvorsen" }); + expect(within(modal).getByLabelText("Veteran pilot")).toHaveTextContent( + `…and ${veteran.name}. ${veteran.bio}`, + ); + }); + + it("shows the veteran's bio, level and Heavy Cargo 1 on the Pilots screen", async () => { + const rosa: Pilot = { + id: "p-vet", + name: veteran.name, + isAvatar: false, + isVeteran: true, + xp: content.config.pilotLevelXpThresholds[1]!, + level: 2, + skills: { "heavy-cargo": 1 }, + skillPointsAvailable: 3, + assignedShipId: null, + status: "idle", + locationId: "earth", + dutyHours: 0, + dutyLimit: 12, + resting: false, + restEndsAt: null, + }; + const base = samplePlayerState({ tutorial: null, bays: [{ id: "b", locationId: "earth" }] }); + renderApp( + fakeApi({ getPlayerState: () => ({ ...base, pilots: [...base.pilots, rosa] }) }), + "/pilots", + ); + const card = (await screen.findByText(veteran.name)).closest("li")!; + expect(within(card).getByLabelText(`${veteran.name} bio`)).toHaveTextContent(veteran.bio); + expect(card).toHaveTextContent("Level 2"); + expect(card).toHaveTextContent("Heavy Cargo 1/5"); + // The avatar's card has no bio line. + expect(screen.queryByLabelText("Captain bio")).not.toBeInTheDocument(); + }); +}); diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 389c666..87dac3e 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -244,6 +244,6 @@ Spec: `memory-bank/change-requests/CR-010-veteran-pilot-profit-share.md` (receiv - [x] **Step 10J.2 — Sim: profit-share pay.** `sim/src/pay.ts` `pilotShare(payout, fuel)` = round(max(0, payout − fuel) × `pilotProfitShare`). Resolution charges it for hired pilots on contract deliveries (avatar, tutorial runs, deadheads: 0), then takes the loan repayment from what remains; `Resolution.pilotPay` records it. The flat `pilotPayPerDelivery` is gone (config, schema, client). The dispatch sheet's net profit uses the share. Test: `profit-share.test.ts` (400/150 → 63; fuel ≥ payout → 0; exemptions; repayment after pay; credits + pay + repayment = payout over 500 random cases); Step 3.17 tests updated. - [x] **Step 10J.3 — Sim: goal change.** Goal 4 is now `veteran-contract`, "Fly a contract with Rosa Kell" (title from `content.veteranPilot.name`), condition `veteran-contracts` ≥ 1; `GoalProgress.veteranContracts`. The server reports 0 until the veteran is stored (10J.4). Tests: `goals.test.ts` (avatar deliveries alone don't complete it; one veteran contract does) and the goal-id updates in server and web goal tests. - [x] **Step 10J.4 — Server: veteran on the deal.** Migration `0007_cr010_veteran_pilot`: `pilots.is_veteran`. `acceptDeal` inserts the veteran in the same transaction: content name, XP at level 2's threshold, Heavy Cargo 1 bought (1 point spent, 3 unspent), at Earth, unassigned. API `Pilot.isVeteran`. Goal progress counts resolved contract deliveries (not deadheads or tutorial runs) by the veteran. Test: `veteran.test.ts` (state after the deal; no veteran on a refused or repeated accept; avatar rests, is swapped out, the veteran flies at once and is paid a profit share, goal 4 ready; avatar deliveries and a veteran deadhead don't count). -- [ ] Step 10J.5 — Client: deal modal and pilot card +- [x] **Step 10J.5 — Client: deal modal and pilot card.** The deal modal says Halvorsen's pilot comes with the business and lists "Pilot: …and Rosa Kell. Halvorsen's pilot for twelve years." (from content). The veteran's Pilots card shows the bio line under the name. Test: `veteran-ui.test.tsx`. - [ ] Step 10J.6 — Client: pilot share in dispatch and summaries - [ ] Step 10J.7 — Plan amendments and regression