diff --git a/DECISIONS.md b/DECISIONS.md
index dc6855a..56155c4 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -1223,3 +1223,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **The veteran is stored like any assigned pilot** (location null, following the Clipper), so state shows them at Earth through the ship, as it does the avatar.
- **The Clipper is named with the next seeded ship name**, after the Courier, the same way bought ships are.
- **The old rest-and-swap test is replaced** by the CR's "both ships can be dispatched immediately" (both in flight at once); the profit-share check moved onto the veteran's Clipper run.
+
+## Step 10M.4
+
+- **"Twelve years" lives in the bio, not the sentence**: the CR's example sentence ("who's flown that Clipper for twelve years") is split so the sentence uses only names (veteran, ships, bay) and the bio carries the history, which also shows on the pilot card.
+- **The loan shown is `content.dealLoanAmount`**; before the deal the player has no loan in state, so "matching state" is tested as the offer equalling the balance the after-deal state reports.
diff --git a/apps/web/src/components/DealModal.tsx b/apps/web/src/components/DealModal.tsx
index d43938e..b547a84 100644
--- a/apps/web/src/components/DealModal.tsx
+++ b/apps/web/src/components/DealModal.tsx
@@ -29,7 +29,10 @@ export function DealModal() {
const { dealLoanAmount } = content;
const { loanRepaymentShare } = content.config;
- const courier = content.shipModel("courier")!;
+ // The deal's ships (decisions 0.5.30, 0.5.82).
+ const courier = content.shipModel(content.config.deal.avatarShipModelId)!;
+ const clipper = content.shipModel(content.config.deal.veteranShipModelId)!;
+ const bay = content.location(content.config.deal.bayLocationId)!;
const veteran = content.veteranPilot;
return (
@@ -41,17 +44,23 @@ export function DealModal() {
layer="z-30"
>
-
+
- 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.
+ Old Halvorsen is retiring. He'll sell you his {courier.name}, his {clipper.name}, and
+ his {bay.name} bay — and {veteran.name}, who flies that {clipper.name}. Pay him back out
+ of every job.
-
You get
- - A {courier.name} and a bay at Earth
+ -
+ A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name}
+
- Pilot
-
…and {veteran.name}. {veteran.bio}
diff --git a/apps/web/test/deal-modal.test.tsx b/apps/web/test/deal-modal.test.tsx
index 95723a5..09fb9cd 100644
--- a/apps/web/test/deal-modal.test.tsx
+++ b/apps/web/test/deal-modal.test.tsx
@@ -14,7 +14,7 @@ const offered = () =>
},
});
-/** State the server returns after the deal: tutorial over, a Courier, a bay, the loan. */
+/** State the server returns after the deal: tutorial over, the Courier and Clipper, a bay, the loan. */
const afterDeal = () =>
samplePlayerState({
tutorial: null,
@@ -36,6 +36,17 @@ const afterDeal = () =>
...shipExtras("courier"),
pilotId: "avatar",
},
+ {
+ id: "s2",
+ modelId: "clipper",
+ name: "Swift Heron",
+ status: "idle",
+ locationId: "earth",
+ dispatchId: null,
+ lastArrivalAt: 0,
+ ...shipExtras("clipper"),
+ pilotId: "veteran",
+ },
],
bays: [{ id: "b1", locationId: "earth" }],
});
@@ -61,7 +72,14 @@ describe("the deal", () => {
"/contracts",
);
const dialog = await screen.findByRole("dialog", { name: "An offer from Halvorsen" });
- expect(dialog).toHaveTextContent("Courier and a bay at Earth");
+ // Both ships and the veteran (CR-013), names from content.
+ const veteran = content.veteranPilot;
+ expect(screen.getByLabelText("Ships and bay")).toHaveTextContent(
+ `A Courier for you, a Clipper for ${veteran.name}, and a bay at Earth`,
+ );
+ expect(dialog).toHaveTextContent("his Courier, his Clipper, and his Earth bay");
+ expect(screen.getByLabelText("Veteran pilot")).toHaveTextContent(veteran.name);
+ expect(screen.getByLabelText("Veteran pilot")).toHaveTextContent(veteran.bio);
expect(screen.getByLabelText("Loan amount")).toHaveTextContent(
`${content.dealLoanAmount.toLocaleString("en-US")} cr`,
);
@@ -119,6 +137,10 @@ describe("the deal", () => {
},
});
renderApp(api, "/contracts");
+ // The loan offered is the loan the server books (CR-013: Courier + Clipper + bay).
+ expect(await screen.findByLabelText("Loan amount")).toHaveTextContent(
+ `${afterDeal().company.loanBalance.toLocaleString("en-US")} cr`,
+ );
await userEvent.click(await screen.findByRole("button", { name: "Shake on it" }));
await waitFor(() => expect(api.call).toHaveBeenCalledWith("acceptDeal", {}));
expect(await screen.findByRole("heading", { level: 1, name: "HQ" })).toBeInTheDocument();
diff --git a/memory-bank/progress.md b/memory-bank/progress.md
index 8e3c89e..ddfe67b 100644
--- a/memory-bank/progress.md
+++ b/memory-bank/progress.md
@@ -290,5 +290,5 @@ Spec: `memory-bank/change-requests/CR-013-veterans-clipper.md` (received as "CR-
- [x] **Step 10M.1 — Content: veteran skill and loan.** The veteran starts with **Expensive Cargo 1**. Config `dealLoanAmount` (5,000) is replaced by `deal: { avatarShipModelId: "courier", veteranShipModelId: "clipper", bayLocationId: "earth" }`, and the loan is computed by `dealLoanAmount({ config, shipModels, locations })` (`packages/content/src/deal.ts`), exposed as `content.dealLoanAmount`: 4,000 + 61,600 + 1,000 = **66,600 cr**. All users (server deal, loan card, deal modal, tests) read `content.dealLoanAmount`. Tests: config (deal ids; loan = Courier + Clipper + Earth lease; it moves by exactly the change when either price or the lease changes), veteran skills (content and server).
- [x] **Step 10M.2 — Sim: goal changes.** Goal 3 is now `market-ship` "Buy a ship from the Market" (condition `market-ships-bought` 1) and goal 4 `priority-contract` "Complete a Priority contract" (`priority-deliveries` 1, any pilot); the `veteran-contracts` condition is gone. `GoalProgress` gains `marketShipsBought` and `priorityDeliveries` (drops `veteranContracts`). Server: `ships.from_deal` (migration 0009, backfilling each player's first ship), set on the deal's ship; goal progress counts non-deal ships and resolved non-tutorial dispatches whose contract is Priority. Tests: sim (the deal's two ships don't complete goal 3, a bought Lighter does; Standard deliveries don't complete goal 4, a Priority one does), server goals (buying a Lighter; a Standard then a Priority delivery), web goal list ids.
- [x] **Step 10M.3 — Server: deal creates the Clipper.** `acceptDeal` reads `config.deal` and inserts both deal ships at Earth (Courier, then Clipper, consecutive seeded names, `from_deal`), assigns the avatar to the Courier and the veteran to the Clipper (location follows the ship), sets the 66,600 cr loan. Tests: `deal.test.ts` (two crewed ships at Earth, one bay, the loan; two simultaneous accepts still give two ships, not four; refused and repeated accepts change nothing), `veteran.test.ts` (veteran crewing the Clipper, level 2, Expensive Cargo 1; the Courier and the Clipper dispatched at the same time, the veteran's run paying their profit share).
-- [ ] Step 10M.4 — Client: deal modal
+- [x] **Step 10M.4 — Client: deal modal.** The modal shows both ship sprites and reads "Old Halvorsen is retiring. He'll sell you his Courier, his Clipper, and his Earth bay — and Rosa Kell, who flies that Clipper. Pay him back out of every job."; "You get" lists "A Courier for you, a Clipper for Rosa Kell, and a bay at Earth"; the Pilot row keeps name and bio. Ship, bay and pilot names come from content (`config.deal`, `veteranPilot`); the veteran's bio is now "Has flown Halvorsen's Clipper for twelve years." Tests (`deal-modal.test.tsx`): both ships, the veteran's name and bio, and the loan offered equal to the loan in the after-deal state.
- [ ] Step 10M.5 — Plan amendments and regression
diff --git a/packages/content/src/data/onboarding.ts b/packages/content/src/data/onboarding.ts
index ded03bb..9827827 100644
--- a/packages/content/src/data/onboarding.ts
+++ b/packages/content/src/data/onboarding.ts
@@ -21,7 +21,7 @@ export const scriptedTutorialContracts: ScriptedTutorialContract[] = [
*/
export const veteranPilot: VeteranPilot = {
name: "Rosa Kell",
- bio: "Halvorsen's pilot for twelve years.",
+ bio: "Has flown Halvorsen's Clipper for twelve years.",
level: 2,
skills: { "expensive-cargo": 1 },
};