diff --git a/DECISIONS.md b/DECISIONS.md index e3c9846..07aa7cc 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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. - **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. diff --git a/memory-bank/change-requests/CR-009-ship-upgrades.md b/memory-bank/change-requests/CR-009-ship-upgrades.md new file mode 100644 index 0000000..00c57e5 --- /dev/null +++ b/memory-bank/change-requests/CR-009-ship-upgrades.md @@ -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. diff --git a/memory-bank/implementation-plan.md b/memory-bank/implementation-plan.md index 942355f..33ff033 100644 --- a/memory-bank/implementation-plan.md +++ b/memory-bank/implementation-plan.md @@ -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. 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). +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 ### Step 11.1 — Installable PWA diff --git a/memory-bank/progress.md b/memory-bank/progress.md index fc9a7e1..435cd23 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -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.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:`, 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.** + +## 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