CR-009: record ship upgrades change request and decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 19:03:36 -04:00
parent 4acf346005
commit bf166c1deb
4 changed files with 158 additions and 0 deletions

View File

@ -919,3 +919,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Fit jobs choose a good, not a class.** Picking a class first (from `cargoClassMix`) and then a good meant a skilled pilot could be locked into one class whose only route good didn't fit the ship: a Heavy Cargo 1 Courier at the Moon got nothing but 5–20 t "Lunar Titanium" (a 30–120 t good). Fit jobs now pick among all qualified goods on the route that fit the slot; the board's class mix for non-fit jobs is untouched. - **Fit jobs choose a good, not a class.** Picking a class first (from `cargoClassMix`) and then a good meant a skilled pilot could be locked into one class whose only route good didn't fit the ship: a Heavy Cargo 1 Courier at the Moon got nothing but 5–20 t "Lunar Titanium" (a 30–120 t good). Fit jobs now pick among all qualified goods on the route that fit the slot; the board's class mix for non-fit jobs is untouched.
- **No undersized lots.** When every good of the chosen class is too heavy for a slot, the slot carries a Standard good on the route that fits instead (the CR-007 fallback, extended to mass). Goods too light for a big band still make bulk lots, as the user chose for CR-007; the Earth class-mix test (±3 points) still passes. - **No undersized lots.** When every good of the chosen class is too heavy for a slot, the slot carries a Standard good on the route that fits instead (the CR-007 fallback, extended to mass). Goods too light for a big band still make bulk lots, as the user chose for CR-007; the Earth class-mix test (±3 points) still passes.
## CR-009 — Ship Upgrades (Phase 10I)
- **Renumbered on arrival:** the request was titled "CR-006 — Ship Upgrades" for "Phase 10F". CR-006 to CR-008 and Phases 10F–10H already exist, so it is stored as **CR-009** and done as **Phase 10I** (its steps 10F.n are 10I.n).
- **Decision numbering:** the CR's 0.5.45–0.5.50 are recorded as **0.5.63 Effective stats, 0.5.64 Where and when, 0.5.65 One level at a time, 0.5.66 Shop time, 0.5.67 Models stay distinct, 0.5.68 Courier to Mars**.
- **Error codes:** kebab-case, reusing existing codes that mean the same: `ship_busy` → `ship-not-idle`, `insufficient_funds` → `insufficient-funds`, `tutorial_active` → `tutorial-active`, `too_slow` → `too-slow`. New: `not-at-hq`, `ship-in-shop`, `upgrade-maxed`, `not-upgradable`.
- **Fits the current build:** the CR assumes Priority windows and a too-slow reason, which CR-007 added.

View File

@ -0,0 +1,125 @@
# Change Request CR-009 — Ship Upgrades
> **Numbering note (added when implemented):** this request arrived titled "CR-006 — Ship Upgrades", for "Phase 10F", applying to "CR-001 through CR-005". CR-006 to CR-008 and Phases 10F–10H already existed, so it is stored as **CR-009** and done as **Phase 10I**: every "10F.n" below means **10I.n**. Its decisions 0.5.45–0.5.50 are recorded as **0.5.63 Effective stats, 0.5.64 Where and when, 0.5.65 One level at a time, 0.5.66 Shop time, 0.5.67 Models stay distinct, 0.5.68 Courier to Mars** (0.5.45–0.5.62 were in use). Error codes use the API's kebab-case and existing codes where one already means the same thing: `not_at_hq` → `not-at-hq`, `ship_busy` → `ship-not-idle`, `ship_in_shop` → `ship-in-shop`, `upgrade_maxed` → `upgrade-maxed`, `insufficient_funds` → `insufficient-funds`, `tutorial_active` → `tutorial-active`, `not_upgradable` → `not-upgradable`; `too_slow` is the existing `too-slow`.
**Project:** Deadhead: Space Freight Tycoon
**Applies to:** a build with Phases 1–10 and CR-001 through CR-005 complete
**Do this:** as **Phase 10F**, after Phase 10E and before Phase 11
**Related docs:** `implementation-plan.md`, the earlier change requests, `game-design-document.md`, `DECISIONS.md`
---
## 1. Why
A ship's stats are fixed by its model, so the only way to improve the fleet is to buy a different ship. There is nothing to spend money on between ship purchases and no way to make a favorite ship better.
This adds an ATS-style tuning shop:
- Five upgrades, each with **three levels**, each improving one ship stat.
- Upgrades are installed **at an HQ (bay)**, which gives bays a second purpose besides hiring pilots.
- The ship is **out of service** while it's in the shop: start an upgrade, put the phone down, come back to a better ship.
- **Caps keep ship models distinct.** A maxed Courier is still a small, fast ship, never a hauler. One deliberate exception: a Courier with maxed fuel tanks can reach Mars around closest approach, as an ambitious early goal.
## 2. Upgrades
All numbers are starting values in `packages/content`. Percentages are of the ship model's base stat and add up across levels (level 3 engines = +30% speed).
| Upgrade id | Name | Improves | Per level | Max level |
|---|---|---|---|---|
| `engines` | Engine Tuning | Cruise speed | +10% | 3 |
| `cargo-bay` | Cargo Bay Expansion | Cargo capacity | +15% | 3 |
| `fuel-tanks` | Extended Fuel Tanks | Fuel range | +20% | 3 (see 10F.1 for the Courier) |
| `landing-thrusters` | Landing Thrusters | Takeoff/landing overhead | −15% | 3 |
| `handling-gear` | Cargo Handling Gear | Handling time per tonne | −15% | 3 |
| Level | Cost (% of ship model's purchase price) | Time in shop (real minutes) |
|---|---|---|
| 1 | 10% | 10 |
| 2 | 20% | 20 |
| 3 | 35% | 30 |
Hull plating is left out until random events exist, because hull rating has no effect yet.
## 3. Rules for this change
- All rules in `implementation-plan.md` §0.1 apply: steps in order, one commit per step, each step's test must pass, `packages/sim` stays pure, all numbers live in `packages/content`.
- Every place that reads a ship's speed, capacity, range, overhead, or handling time must use the ship's **effective stats** (model plus upgrades), never the model's stats directly.
- Resetting the dev and test databases is fine. Schema migrations are still required.
- Update existing tests whose expectations change. Update `architecture.md` for schema or API changes and `progress.md` after every step.
- Stop for the user's review at the end of Phase 10F.
## 4. Decisions
Record these in `DECISIONS.md` and `implementation-plan.md` §0.5.
- **0.5.45 Effective stats.** A ship's effective stats are its model's base stats adjusted by its installed upgrade levels. Percentage bonuses are of the base stat and add up linearly.
- **0.5.46 Where and when.** An upgrade can be started only on an owned ship that is idle (not in flight, not already in the shop) at a location with a player bay, with tutorial mode off. The Company Courier loaner can't be upgraded.
- **0.5.47 One level at a time.** Each purchase raises one upgrade by exactly one level. A ship can be in the shop for one upgrade at a time.
- **0.5.48 Shop time.** Starting an upgrade deducts the cost, raises the level immediately, and sets the ship's in-shop-until time. The ship can't be dispatched until then (`ship_in_shop`). Because dispatch is blocked, there is nothing to complete later: no background job is needed. The assigned pilot stays assigned and can rest meanwhile.
- **0.5.49 Models stay distinct.** Content caps guarantee: a maxed Courier's capacity is below a base Freighter's; a maxed Bulk Hauler's speed is below a base Freighter's; a maxed Freighter's range is below a base Bulk Hauler's (so only Bulk Haulers reach Jupiter at its farthest).
- **0.5.50 Courier to Mars.** A Courier with maxed fuel tanks can reach Mars at and around closest approach, but not at Mars's farthest distance. Base and lower-level Couriers still can't reach Mars at all.
## 5. Steps — Phase 10F
### Step 10F.1 — Content: upgrades
- Add an Upgrade schema: id, name, stat affected, change per level (percent), max level, and optional per-model max level overrides.
- Add the five upgrades from §2, and config for cost percent and shop time per level.
- Set the Courier's `fuel-tanks` values so decision 0.5.50 holds (adjust the Courier's per-level bonus or cap, not the Courier's base range, and don't break any Step 3.7 range test).
- **Test:** All upgrades validate. Every upgrade affects a stat that exists on ShipModel. Tests for decision 0.5.49 (all three inequalities). Tests for decision 0.5.50: sampling Earth–Mars distance monthly over two game years, a maxed Courier is in range on at least one sample and out of range on at least one sample; a Courier with fuel tanks at level 2 is never in range.
### Step 10F.2 — Sim: effective stats
- Add a pure function that returns a ship's effective stats from its model and installed upgrade levels.
- Change travel time, fuel range checks, capacity checks, handling time, and Priority window checks to take effective stats.
- **Test:** With no upgrades, effective stats equal the model's stats exactly, and all existing Phase 3 tests pass unchanged. Each upgrade changes only its own stat. Level 3 engines give exactly +30% speed. A cargo-bay upgrade turns a contract that failed with the too-heavy reason into a valid one. An engines upgrade turns a contract that failed with `too_slow` into a valid one.
### Step 10F.3 — Sim: upgrade validation and cost
- Add pure functions: next-level cost and shop time for a ship and upgrade; validation for starting an upgrade per decisions 0.5.46 and 0.5.47, with reasons: `not_at_hq`, `ship_busy` (in flight), `ship_in_shop`, `upgrade_maxed`, `insufficient_funds`, `tutorial_active`, `not_upgradable` (the loaner).
- Dispatch validation gains `ship_in_shop`.
- **Test:** One test per reason. Cost for level 1 on a Freighter equals 10% of the Freighter's price; level 3 equals 35%. A ship in the shop fails dispatch validation with `ship_in_shop` one second before in-shop-until and passes at it.
### Step 10F.4 — Schema and database
- Add a `ship_upgrades` table (ship, upgrade id, level) and an in-shop-until time on ships.
- Add a "start upgrade" endpoint (ship id, upgrade id) to the route catalog, and the new error codes.
- Player state includes, per ship: installed upgrade levels, effective stats, and in-shop-until.
- Update `architecture.md`.
- **Test:** Migrations run on a fresh database and are a no-op the second time. New and changed schemas accept valid examples and reject invalid ones. The route catalog test still passes.
### Step 10F.5 — Server: start upgrade
- In one transaction with the company row lock (Step 6.14): validate, deduct the cost, raise the level by one, set in-shop-until.
- Dispatch uses effective stats and returns `ship_in_shop` for ships in the shop.
- **Test:** Starting a level 1 engines upgrade on an idle Freighter at the Earth HQ deducts the right cost, sets the level to 1 and the correct in-shop-until. Dispatching it before then fails with `ship_in_shop`; after, it succeeds and its trip time reflects the faster speed. Starting an upgrade at a location without a bay fails with `not_at_hq`. Two simultaneous upgrade requests the player can afford only one of produce exactly one upgrade and no negative balance.
### Step 10F.6 — Client: upgrade shop
- Each ship on the Fleet screen opens a detail view with an **Upgrades** panel listing all five upgrades: current level, next level's effect as before → after (for example "Speed 180 → 198 km/h"), cost, and shop time.
- Disabled upgrades show the reason (not at an HQ, in flight, in the shop, maxed, can't afford).
- Confirming starts the upgrade. Ships in the shop show "In the shop — mm:ss" on their Fleet card.
- **Test:** Component test: before → after values match the sim's effective stats; each disabled reason displays correctly; confirming calls the endpoint; an in-shop ship shows the countdown.
### Step 10F.7 — Client: effective stats everywhere
- Contract card badges (TOO HEAVY, TOO SLOW), the dispatch sheet, the deadhead picker, the Market's ship comparison for owned ships, and map reachability text all use effective stats.
- **Test:** Component test: after a seeded cargo-bay upgrade, a contract that showed TOO HEAVY for that ship no longer does; the deadhead picker enables a destination that only the upgraded range reaches.
### Step 10F.8 — Client: shop in summaries and on HQ
- The "while you were away" modal shows "[Ship] is out of the shop: Engine Tuning 2" for upgrades that finished while the player was away.
- HQ shows ships currently in the shop with countdowns.
- **Test:** Component test with seeded state: the modal line appears for an upgrade that finished since the last visit; HQ lists in-shop ships with correct countdowns.
### Step 10F.9 — Plan amendments and regression
- Apply the amendments in §6 to `implementation-plan.md`, and update `game-design-document.md` §8 (done by the designer; confirm it matches).
- Run the full suite.
- **Test:** CI is green. Manual check: on a fresh account after the CR-001 deal, earn enough to buy Cargo Bay Expansion 1 for the Courier at the Earth HQ, confirm it's unavailable for the shop time, then confirm it can take a contract that was too heavy before.
## 6. Amendments to `implementation-plan.md`
| Step | Change |
|---|---|
| 0.5 | Add decisions 0.5.45–0.5.50. |
| 2.1 | Content gains the Upgrade schema and upgrade cost/time config (10F.1). |
| 3.5, 3.7, 3.12 | Use effective stats; validation gains `ship_in_shop` (10F.2, 10F.3). |
| 5.3 | `ship_upgrades` table and in-shop-until on ships (10F.4). |
| 9.5, 9.6, 9.8 | Effective stats in dispatch, Fleet, and Market; upgrade shop on Fleet (10F.6, 10F.7). |
| 12.1 | The simulated player may buy upgrades: when it can't yet afford its next planned ship, it buys the cheapest available upgrade on its most-used ship, preferring cargo bay and engines. Re-check all timing targets; adjust content values, not thresholds. |
## 7. Out of scope
Hull plating (until random events exist), selling or removing upgrades, ship resale value, changing a ship's landing class, cosmetic liveries, ship renaming, and anything else in `implementation-plan.md` §0.3.

View File

@ -118,6 +118,12 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
60. **Market rate** (CR-008). Each board contract (not tutorial) gets its own pay rate, uniform within ±`contractPayVariance` of 1, multiplied into its payout; the average is unchanged. 60. **Market rate** (CR-008). Each board contract (not tutorial) gets its own pay rate, uniform within ±`contractPayVariance` of 1, multiplied into its payout; the average is unchanged.
61. **More early goods** (CR-008). Water Recyclers, Spare Parts and Lunar Base Supplies (Earth → Moon), Crew Personal Effects and Recycled Alloys (Moon → Earth) join the catalog. 61. **More early goods** (CR-008). Water Recyclers, Spare Parts and Lunar Base Supplies (Earth → Moon), Crew Personal Effects and Recycled Alloys (Moon → Earth) join the catalog.
62. **Visible refresh** (CR-008). When a location's board changes, the screen announces "New contracts posted" and tags contracts not on the last board the player saw there "New" (remembered per location in the browser; nothing tagged on a first visit). 62. **Visible refresh** (CR-008). When a location's board changes, the screen announces "New contracts posted" and tags contracts not on the last board the player saw there "New" (remembered per location in the browser; nothing tagged on a first visit).
63. **Effective stats** (CR-009). A ship's effective stats are its model's base stats adjusted by its installed upgrade levels. Percentage bonuses are of the base stat and add up linearly.
64. **Where and when** (CR-009). An upgrade can be started only on an owned ship that is idle (not in flight, not already in the shop) at a location with a player bay, with tutorial mode off. The Company Courier loaner can't be upgraded.
65. **One level at a time** (CR-009). Each purchase raises one upgrade by exactly one level. A ship can be in the shop for one upgrade at a time.
66. **Shop time** (CR-009). Starting an upgrade deducts the cost, raises the level immediately, and sets the ship's in-shop-until time. The ship can't be dispatched until then (`ship-in-shop`). Because dispatch is blocked, there is nothing to complete later: no background job is needed. The assigned pilot stays assigned and can rest meanwhile.
67. **Models stay distinct** (CR-009). Content caps guarantee: a maxed Courier's capacity is below a base Freighter's; a maxed Bulk Hauler's speed is below a base Freighter's; a maxed Freighter's range is below a base Bulk Hauler's (so only Bulk Haulers reach Jupiter at its farthest).
68. **Courier to Mars** (CR-009). A Courier with maxed fuel tanks can reach Mars at and around closest approach, but not at Mars's farthest distance. Base and lower-level Couriers still can't reach Mars at all.
--- ---
@ -573,6 +579,12 @@ Steps 10H.1–10H.4 are specified in `memory-bank/change-requests/CR-008-fresh-b
--- ---
## Phase 10I — Ship Upgrades (CR-009)
Steps 10I.1–10I.9 are specified in `memory-bank/change-requests/CR-009-ship-upgrades.md` §5 (written there as 10F.1–10F.9), with its §6 amendments to earlier steps. Done after Phase 10H and before Phase 11; stop for review at the end.
---
## Phase 11 — PWA ## Phase 11 — PWA
### Step 11.1 — Installable PWA ### Step 11.1 — Installable PWA

View File

@ -221,3 +221,17 @@ Spec: `memory-bank/change-requests/CR-008-fresh-boards.md`. New decisions record
- [x] **Step 10H.2 — Sim: per-contract pay rate.** `PayoutInput.marketRate` (default 1) multiplies the payout; `payRate(rng, content)` draws 1 ± `contractPayVariance` uniformly as the last draw of every board contract (fit and normal); `buildContract` takes `payRate` (tutorial boards pass none, so tutorial payouts are unchanged). Tests: `pay-rate.test.ts` (payout scales with the rate; 1,000 Earth boards: rates within ±15 %, mean within 1 ± 0.01; the same good, mass and route now pays different amounts); the tutorial and CR-007 class-mix tests still pass. - [x] **Step 10H.2 — Sim: per-contract pay rate.** `PayoutInput.marketRate` (default 1) multiplies the payout; `payRate(rng, content)` draws 1 ± `contractPayVariance` uniformly as the last draw of every board contract (fit and normal); `buildContract` takes `payRate` (tutorial boards pass none, so tutorial payouts are unchanged). Tests: `pay-rate.test.ts` (payout scales with the rate; 1,000 Earth boards: rates within ±15 %, mean within 1 ± 0.01; the same good, mass and route now pays different amounts); the tutorial and CR-007 class-mix tests still pass.
- [x] **Step 10H.3 — Client: visible refresh.** `useNewContracts(locationId, contracts)` (`LocationBoards.tsx`) compares the board with the last one seen at that location (`localStorage` key `deadhead:seen-board:<id>`, read and written in try/catch), then remembers it. Contracts not seen before get a cyan "New" pill and rise in; the refresh line shows a `role="status"` "New contracts posted". Tags stay until the board changes again (contracts taken in between drop out); nothing is tagged on a first visit. The test setup clears `localStorage` after each test. Test: `fresh-board.test.tsx` (remembered older board → tags and notice; same board → none; first visit → none but remembered; rollover while open → notice and tags on the new board). - [x] **Step 10H.3 — Client: visible refresh.** `useNewContracts(locationId, contracts)` (`LocationBoards.tsx`) compares the board with the last one seen at that location (`localStorage` key `deadhead:seen-board:<id>`, read and written in try/catch), then remembers it. Contracts not seen before get a cyan "New" pill and rise in; the refresh line shows a `role="status"` "New contracts posted". Tags stay until the board changes again (contracts taken in between drop out); nothing is tagged on a first visit. The test setup clears `localStorage` after each test. Test: `fresh-board.test.tsx` (remembered older board → tags and notice; same board → none; first visit → none but remembered; rollover while open → notice and tags on the new board).
- [x] **Step 10H.4 — Regression, and a fix found by the user.** Plan amendments for steps 3.8 and 9.4. Manual check across a real rollover (`docs/screenshots/10h4-board-rollover-desktop.png`): one second after the timer hit zero the open page showed "New contracts posted" and tagged every new card New. **User report after restarting:** "the jobs are the same". Their Moon boards had six of eight jobs as Lunar Titanium every period: a fit job picked a class first, and for a Heavy Cargo 1 pilot at the Moon that was only Heavy, whose one route good (Lunar Titanium, 30–120 t) became an undersized bulk lot for the Courier; open jobs did the same. Fix (`board.ts`): fit jobs choose among every good on the route the crew is qualified for and whose range fits the slot and ship (`fitsSlot`), falling back to the class choice only when none does; `slotGoods` swaps a class whose goods are all too heavy for the slot for Standard goods on the route (goods too light for a big band still become bulk lots). With the user's save the next Moon boards carry Optical Glass, Souvenirs, Crew Personal Effects and Recycled Alloys. Test: `board-fleet.test.ts` (the user's Courier at the Moon: ≥ 5 flyable jobs per board, every flyable job's mass inside its good's range, no Lunar Titanium, ≥ 3 distinct goods per board on average); the class-mix test still passes. Full suite (110 files, 763 tests) green. **Phase 10H complete — stopped for review.** - [x] **Step 10H.4 — Regression, and a fix found by the user.** Plan amendments for steps 3.8 and 9.4. Manual check across a real rollover (`docs/screenshots/10h4-board-rollover-desktop.png`): one second after the timer hit zero the open page showed "New contracts posted" and tagged every new card New. **User report after restarting:** "the jobs are the same". Their Moon boards had six of eight jobs as Lunar Titanium every period: a fit job picked a class first, and for a Heavy Cargo 1 pilot at the Moon that was only Heavy, whose one route good (Lunar Titanium, 30–120 t) became an undersized bulk lot for the Courier; open jobs did the same. Fix (`board.ts`): fit jobs choose among every good on the route the crew is qualified for and whose range fits the slot and ship (`fitsSlot`), falling back to the class choice only when none does; `slotGoods` swaps a class whose goods are all too heavy for the slot for Standard goods on the route (goods too light for a big band still become bulk lots). With the user's save the next Moon boards carry Optical Glass, Souvenirs, Crew Personal Effects and Recycled Alloys. Test: `board-fleet.test.ts` (the user's Courier at the Moon: ≥ 5 flyable jobs per board, every flyable job's mass inside its good's range, no Lunar Titanium, ≥ 3 distinct goods per board on average); the class-mix test still passes. Full suite (110 files, 763 tests) green. **Phase 10H complete — stopped for review.**
## Phase 10I — Ship Upgrades (CR-009)
Spec: `memory-bank/change-requests/CR-009-ship-upgrades.md` (received as "CR-006 … Phase 10F"). New decisions recorded as plan §0.5.63–0.5.68.
- [ ] Step 10I.1 — Content: upgrades
- [ ] Step 10I.2 — Sim: effective stats
- [ ] Step 10I.3 — Sim: upgrade validation and cost
- [ ] Step 10I.4 — Schema and database
- [ ] Step 10I.5 — Server: start upgrade
- [ ] Step 10I.6 — Client: upgrade shop
- [ ] Step 10I.7 — Client: effective stats everywhere
- [ ] Step 10I.8 — Client: shop in summaries and on HQ
- [ ] Step 10I.9 — Plan amendments and regression