diff --git a/DECISIONS.md b/DECISIONS.md index f3e3d27..0733055 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1493,3 +1493,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Body positions are computed once per app load**, not per frame: nothing about them can change. The per-frame scene still rebuilds flights, idle counts and discovery, which do. - **The date-free wording drops "now" too** ("From Earth: …" rather than "From Earth now: …"): with a fixed map there is no other time to contrast it with. + +## Step 15.9 + +- **"Never needs emergency fuel" is checked where the player decides**: each time an idle ship is about to choose work, after its "Refuel here first" top-up. Checking only at landings would miss a ship that ran down by deadheading. +- **Plan notes, not rewrites**, as for every earlier change request: each amended decision or step says what CR-015 changed and points to the step. diff --git a/memory-bank/implementation-plan.md b/memory-bank/implementation-plan.md index 8901c11..91e3b76 100644 --- a/memory-bank/implementation-plan.md +++ b/memory-bank/implementation-plan.md @@ -36,7 +36,7 @@ This plan covers the **base game only**: the core loop of taking contracts, disp ### 0.3 Explicitly deferred (do NOT build) -Random events, reputation, fuel price variation, throttle setting, multi-leg routes, recurring bay rent, pilot salaries over time, pilot poaching, automation/standing routes, interstellar, push notifications, Capacitor builds, Hardened landing class, Venus/Io, Fragile/Urgent/Fuel-efficiency skills, social features, monetization, account linking (email, Apple, Google sign-in) *(CR-011)*. +Random events, reputation, fuel price drift and depot shortages *(CR-015: fixed per-location prices are in, decision 0.5.96)*, fuel storage at HQ bays, throttle setting, multi-leg routes, recurring bay rent, pilot salaries over time, pilot poaching, automation/standing routes, interstellar, push notifications, Capacitor builds, Hardened landing class, Venus/Io, Fragile/Urgent/Fuel-efficiency skills, social features, monetization, account linking (email, Apple, Google sign-in) *(CR-011)*. ### 0.4 Glossary @@ -64,10 +64,10 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple 6. **Cargo tier.** Standard is tier 0. A non-Standard contract has a tier from 1 to 5, and that tier is the required level in the cargo's skill. *Amended by 0.5.55 (CR-007): High-Value and Dangerous tiers come from the good.* 7. **Long Haul.** A config list of distance cut-offs, one per Long Haul level. The required Long Haul level is the number of cut-offs the route distance exceeds. 8. **Config-driven tuning.** First-contact chance, first-contact multiplier, contract mass range, and Long Haul cut-offs all live in GameConfig. *(Amended after the CR-001 playtest: the single mass range is replaced by per-ship-size mass bands, `contractMassBands`.)* -9. **Game epoch.** Two fields: a real start timestamp and the in-game ephemeris date it maps to. The clock is global for all players. +9. **Game epoch.** Two fields: a real start timestamp and the in-game ephemeris date it maps to. The clock is global for all players. *(Amended by CR-015, decision 0.5.93: the in-game date is only displayed; positions are fixed, so `ephemerisDate` became `gameStart`.)* 10. **World seed** is per player and is also used for pilot and ship names. -11. **Payout** is fixed when a contract is offered, using the distance at that moment. Travel time and fuel use the distance at departure. -12. **Pilot pay.** *(Replaced by CR-010.)* Hired pilots earn `max(0, payout − fuel cost) × pilot profit share` per contract delivery. The avatar is exempt. Deadheads and tutorial runs pay nothing. +11. **Payout** is fixed when a contract is offered, using the distance at that moment. Travel time and fuel use the distance at departure. *(Since CR-015 the distance never changes, decision 0.5.93.)* +12. **Pilot pay.** *(Replaced by CR-010.)* Hired pilots earn `max(0, payout − fuel cost) × pilot profit share` per contract delivery. The avatar is exempt. Deadheads and tutorial runs pay nothing. *(Amended by CR-015, decision 0.5.98: "fuel cost" is the fuel burned × the tank's average purchase price.)* 13. **Hull rating** is stored but has no effect in the base game. 14. **Ship names** are generated from the world seed. No renaming. 15. **Bays** have no ship limit. @@ -80,7 +80,7 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple 22. **Pilot endpoints.** The route catalog includes "unassign pilot", and "hire pilot" takes the bay location to hire at. 23. **Avatar during tutorial.** The avatar is locked to the loaner and cannot be assigned to an owned ship until the tutorial ends. 24. **Repositions need no skills.** A reposition checks range and landing only; no Long Haul or cargo skill is required. -25. **Freighter range** covers Ceres at its farthest from Earth (≈600M km), so it can also reach Jupiter at its closest. *(Superseded by CR-012, decision 0.5.81: a Freighter reaches Ceres from Earth on most dates, not all, and never reaches Jupiter from Earth; it gets there by hopping via Ceres.)* A ship left out of range of every bay waits until distances shrink; this is intended. +25. **Freighter range** covers Ceres at its farthest from Earth (≈600M km), so it can also reach Jupiter at its closest. *(Superseded by CR-012, decision 0.5.81: a Freighter reaches Ceres from Earth on most dates, not all, and never reaches Jupiter from Earth; it gets there by hopping via Ceres.)* A ship left out of range of every bay waits until distances shrink; this is intended. *(CR-015: a yes/no — a Freighter reaches Ceres from Earth; Jupiter only from Ceres.)* 26. **Loaner in player state.** Player state has a `tutorial` object with the loaner's location and whether a run is in flight. The loaner is not in the ships list. 27. **Pilot location storage.** Only unassigned pilots store a location; an assigned pilot's location is derived from their ship. 28. **Git hosting.** The repo is on a self-hosted Gitea instance: `http://192.168.1.107:3000/rory/deadhead-space-freight-tycoon`. CI uses Gitea Actions. @@ -123,7 +123,7 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple 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). *(Amended by CR-012:)* also a maxed Clipper's capacity is below a base Freighter's; a maxed Courier's speed is below a base Clipper's; a maxed Lighter can't reach Mars from Earth or the Moon on any sampled date; "only Bulk Haulers reach Jupiter at its farthest" is measured from Earth. -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. *(Amended by CR-012:)* measured from Earth; from the Moon it reaches Mars more often, which is fine. +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. *(Amended by CR-012:)* measured from Earth; from the Moon it reaches Mars more often, which is fine. *(Amended by CR-015:)* on the fixed map, maxed Extended Fuel Tanks reach Mars from Earth; level 2 can't, from Earth or the Moon. 69. **Deduction order** (CR-010). On resolution: pilot pay is taken from the payout first; loan repayment (0.5.32) is calculated on what remains. 70. **Veteran pilot** (CR-010). Accepting the deal also creates the veteran (CR-010 §2): at Earth, unassigned, level 2 with Heavy Cargo 1. The veteran is a normal hired pilot in every respect except how they were acquired. *(Amended by CR-013:)* the veteran starts with **Expensive Cargo 1** (was Heavy Cargo 1) and crewing the deal's Clipper (0.5.82), not unassigned. 71. **No accounts yet** (CR-011). Players remain guests. Account linking (email, Apple, Google) stays deferred (§0.3). *(Replaced by CR-014: no accounts at all; the company is created on the device.)* @@ -133,13 +133,13 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple 75. **Company name** (CR-011). Players can rename their company: 3–32 characters, letters, digits, spaces, and basic punctuation, trimmed. The default name is generated from the world seed. 76. **Display preferences** (CR-011). Clock defaults to the device's locale format. Countdowns default to "real time". Reduced motion defaults to the device's reduced-motion setting and can be overridden. 77. **Per-leg range** (CR-012). Ship range is checked per leg, from the departure location, at departure time. Design targets and tests name their origin explicitly. -78. **Gravity-well range factor** (CR-012). Each location has a launch range factor (Earth 1.0, Mars 1.15, Moon 1.3, Ceres Station 1.3, Jupiter Cloud Platform 1.0). Effective range from an origin = ship effective range × origin factor. Separate from the launch fuel-cost multiplier. -79. **No-strand rule** (CR-012). A dispatch (contract or deadhead) is rejected with `would-strand` if, at arrival plus a configured margin (48 game hours), the ship could not reach any other discovered location from the destination with its effective range from there. +78. **Gravity-well range factor** (CR-012). Each location has a launch range factor (Earth 1.0, Mars 1.15, Moon 1.3, Ceres Station 1.3, Jupiter Cloud Platform 1.0). Effective range from an origin = ship effective range × origin factor. Separate from the launch fuel-cost multiplier. *(Amended by CR-015, decision 0.5.95:)* the factor now shortens a leg before its fuel is worked out, so a full tank reaches exactly the old effective range; the launch fuel-cost multiplier is replaced by fixed fuel prices (0.5.96). +79. **No-strand rule** (CR-012). A dispatch (contract or deadhead) is rejected with `would-strand` if, at arrival plus a configured margin (48 game hours), the ship could not reach any other discovered location from the destination with its effective range from there. *(Amended by CR-015:)* no time margin; the ship must be able to leave the destination on a full tank where fuel is sold, otherwise on the fuel left after the leg. 80. **New ships** (CR-012). Lighter (short-haul heavy lifter) and Clipper (fast long-legged runner), purchasable, atmospheric. -81. **Reach targets** (CR-012). CR-012 §3's reach table is the source of truth for ship range values; ship ranges (not location factors) are adjusted so every cell holds. +81. **Reach targets** (CR-012). CR-012 §3's reach table is the source of truth for ship range values; ship ranges (not location factors) are adjusted so every cell holds. *(Amended by CR-015:)* CR-015 §4's yes/no table replaces it; tank capacities and burn rates are sized to it. 82. **The veteran's Clipper** (CR-013). Accepting the deal also creates an owned Clipper at Earth with the veteran assigned to it. 83. **Deal loan** (CR-013). The deal loan amount is Courier price + Clipper price + Earth bay lease, computed from content. -84. **Economic distance** (Step 12.1, user decision). Contract pay and fuel use `reference × (distance / reference)^exponent` instead of the plain distance (reference 0.384M km, Earth–Moon; exponent 0.65), so a Moon run is unchanged and long legs pay and cost on a gentler curve. Range, travel time, Long Haul and XP keep the plain distance. +84. **Economic distance** (Step 12.1, user decision). Contract pay and fuel use `reference × (distance / reference)^exponent` instead of the plain distance (reference 0.384M km, Earth–Moon; exponent 0.65), so a Moon run is unchanged and long legs pay and cost on a gentler curve. Range, travel time, Long Haul and XP keep the plain distance. *(CR-015, user decision:)* a leg's fuel burn follows the same curve (decision 0.5.95). 85. **A first contact the fleet can fly** (Step 12.1). While some undiscovered place is reachable, one fit slot on each board is a first-contact job sized and skilled for a ship that can fly it from there (decision 0.5.52's fit rules), so exploring never waits on board luck. 86. **Hosting** (Phase 13, user decision). The game runs on a home server with Docker Compose at deadhead.belowland.com; a Caddy container in the stack terminates HTTPS (Let's Encrypt), serves the built web app and proxies `/api`; nightly Postgres dumps go to a mounted folder or NAS share. 87. **Local-first** (CR-014). The game runs entirely on the device. There is no game server. @@ -217,12 +217,14 @@ The game has had no server since CR-014. When a later or unfinished change reque - **Amended by CR-010:** GameConfig's flat `pilotPayPerDelivery` is replaced by `pilotProfitShare` (0.25); content gains the veteran pilot (Step 10J.1). - **Amended by CR-012:** Location gains `launchRangeFactor`; ShipModel gains a `role` line and an optional own `handlingGameHoursPerTonne`; GameConfig gains `noStrandMarginGameHours` (Step 10L.1). +- **Amended by CR-015:** Location replaces its astronomy body with a fixed `position` (and `orbitParent` for the Moon) and its launch cost multiplier with `fuelPrice` and `sellsFuel`; ShipModel replaces fuel range and fuel cost with `tankCapacityFu` and `burnFuPerMillionKm`; GameConfig's epoch has `gameStart` instead of `ephemerisDate`, gains `baseFuelPricePerFu`, `emergencyFuelMultiplier` and `deal.tankShare`, and loses `noStrandMarginGameHours` (Steps 15.1–15.4). ### Step 2.2 — Add the five locations - Add data for Earth, Moon, Mars, Ceres Station, and Jupiter Cloud Platform following the destinations table in the GDD. - Earth and Moon start discovered. Earth is atmospheric with the highest launch multiplier; Moon is vacuum with a low multiplier; Mars is atmospheric; Ceres is a belt station (vacuum); Jupiter Cloud Platform is a cloud platform requiring atmospheric landing. - **Test:** A test loads all locations through the schema without errors, asserts exactly five locations, asserts exactly Earth and Moon start discovered, and asserts every cargo id referenced in produces/demands exists. - **Amended by CR-007:** `produces`/`demands` list good ids (derived from the goods catalog); the old class lists are kept as `cargoClassMix`, and each location has `shippers` (Step 10G.1). +- **Amended by CR-015:** Locations sit at fixed positions (Earth 150M km/210°, Mars 228/270°, Ceres 414/285°, Jupiter 778/300°; the Moon 0.384M km out from Earth) and sell fuel at fixed prices, cheapest at Jupiter, dearest at Earth (Steps 15.1, 15.3). ### Step 2.3 — Add the three ship models - Courier: fastest, smallest hold, short range, atmospheric, lowest price. - Freighter: balanced, atmospheric, mid price. @@ -265,10 +267,12 @@ The game has had no server since CR-014. When a later or unfinished change reque - Earth and Mars use their own heliocentric positions. Moon uses Earth's position plus a small fixed offset. Ceres uses its orbital elements (or a fixed-radius circular orbit approximation if the library does not provide Ceres). Jupiter Cloud Platform uses Jupiter's position. - **Test:** For a fixed known date, Earth is about 150 million km from the Sun, Mars between 206 and 250 million km, Jupiter between 740 and 820 million km. Calling the function twice with the same time returns identical values. +- **Amended by CR-015:** Positions are fixed: a location's position comes from content and takes no time; `astronomy-engine` is removed (Steps 15.1–15.2). ### Step 3.4 — Route distance - Add a function returning the distance between two locations at a given game time. - **Test:** Earth→Moon is under 1 million km at any time. Earth→Mars distance varies by at least 200 million km across a 2-game-year span (sample monthly). Distance A→B equals B→A. +- **Amended by CR-015:** Distances are fixed: no time argument, and the "varies" test is replaced by "the same value every time" (Step 15.2). ### Step 3.5 — Travel time - Add a function: travel time = distance ÷ ship cruise speed + ship takeoff/landing overhead. Distance is measured at departure time (no mid-flight recalculation). - **Test:** For fixed inputs, the result matches a hand-calculated value. A faster ship always arrives sooner on the same route. Overhead dominates on Earth→Moon (overhead is more than half of total time for the Courier). @@ -279,6 +283,7 @@ The game has had no server since CR-014. When a later or unfinished change reque - Add a function: fuel cost = distance × ship fuel cost per million km × origin location's launch multiplier. - **Test:** Earth→Moon costs more than Moon→Earth for the same ship. Fuel cost scales linearly with distance. +- **Amended by CR-015:** Replaced by tanks: a leg burns burn rate × economic distance of (distance ÷ origin range factor) from the ship's tank; ships refuel at fixed local prices; a leg's fuel cost is fuel burned × the tank's average price (Step 15.4). ### Step 3.7 — Range and landing eligibility - Add a function that decides whether a ship can fly a route at a given time, returning either "allowed" or a specific reason: out of range, or cannot land at destination (landing class). - **Test:** A Courier can fly Earth→Moon and Moon→Earth. A Courier is out of range for Mars at its closest. A test-only vacuum ship fixture is rejected at Earth with the cannot-land reason. At Jupiter's farthest point from Earth, only the Bulk Hauler is in range. A Freighter can reach Ceres. (These confirm the Step 2.3 number choices; adjust content if they fail.) @@ -319,6 +324,7 @@ The game has had no server since CR-014. When a later or unfinished change reque - **Amended by CR-009:** Validation uses effective stats (capacity, range, Priority window) and gains `ship-in-shop` (Steps 10I.2, 10I.3). - **Amended by CR-012:** Validation (contract and deadhead) gains `would-strand` after range and landing: at arrival plus the no-strand margin, the ship must be able to reach another discovered location from the destination (Step 10L.3, decision 0.5.79). +- **Amended by CR-015:** "Player can afford fuel" becomes `not-enough-fuel` (fuel on board for the leg); `would-strand` uses the fuel the ship could have at the destination (Steps 15.4–15.5). ### Step 3.13 — Dispatch record creation - Add a function that turns a validated dispatch into a Dispatch record: id, ship, pilot, contract (or none for reposition), origin, destination, departure real time, arrival real time (computed from travel time and clock conversion), fuel cost, expected payout, seed. - **Test:** Arrival time minus departure time equals the travel time converted to real time. Creating two records from identical inputs yields identical records. @@ -557,12 +563,14 @@ The game has had no server since CR-014. When a later or unfinished change reque - **Amended by CR-009:** The dispatch sheet uses effective stats and lists "In the shop — mm:ss" for ships in the shop (Step 10I.7). - **Amended by CR-010:** The dispatch sheet shows a "Pilot share" line for hired pilots, and net profit accounts for it (Step 10J.6). +- **Amended by CR-015:** Each pair shows fuel needed, on board and left after; the cost cell is "Fuel used (at your average price)"; a pair short only of fuel offers "Refuel here first" (Step 15.7). ### Step 9.6 — Fleet screen - List all ships: name, model, location or route, assigned pilot, and for in-flight ships a progress bar and countdown. Idle ships at a bay location have a "Reposition" action to another bay location. - **Test:** Component test: an in-flight ship's progress bar matches sim progress for the mocked time. Reposition opens a destination picker listing only bay locations. - **Amended by CR-002:** "Reposition" becomes "Deadhead (fly empty)" for every idle ship, with fuel, time and disabled-with-reason destinations (Step 10B.8). - **Amended by CR-009:** Fleet cards open the upgrade shop and show "In the shop — mm:ss"; the deadhead picker uses effective stats (Steps 10I.6, 10I.7). +- **Amended by CR-015:** Each ship card shows a fuel gauge and refuel policy; a Fuel sheet refuels, switches policy and offers emergency fuel; the deadhead picker shows fuel lines and "Refuel here first" (Step 15.7). ### Step 9.7 — Pilots screen - List pilots: name, level, XP progress to next level, skill levels, available skill points, assignment, status. Actions: hire pilot (shows cost), assign to ship, spend skill point. - **Test:** Component test: spending a point calls the endpoint and the UI updates; hire is disabled with insufficient credits; assign lists only idle ships at the pilot's location. @@ -580,6 +588,7 @@ The game has had no server since CR-014. When a later or unfinished change reque - **Test:** Component test: undiscovered locations hide details; leasing is available only for discovered locations without a bay. - **Amended by CR-002:** Every discovered location shows "View contracts"; the lease action explains "Open an HQ here: hire pilots and take delivery of new ships at this location." (Step 10B.9). +- **Amended by CR-015:** Each location shows its fuel price, the cheapest marked (Step 15.7). ### Step 9.10 — Empty and error states - Every screen has a sensible empty state (e.g., "No ships yet — visit the Market") and a retry option on network failure. - **Test:** Component tests for each screen's empty state and network-error state. @@ -593,6 +602,7 @@ The game has had no server since CR-014. When a later or unfinished change reque - **Test:** Unit test confirms the scale function is monotonic (farther in reality → farther on screen). Manual check: all five locations are visible on a phone-sized screen at default zoom; pinch-zoom works on a touch device. - **Amended by CR-003:** Bodies use sprites with a roughly constant screen size (zoom^0.35, clamped 0.75–1.75×) and the Moon at a fixed screen offset from Earth (Step 10C.6, decisions 0.5.38–0.5.39). +- **Amended by CR-015:** Bodies sit at fixed positions, placed once; orbit rings are static decoration; location panels show the fuel price and date-free reach (Steps 15.7–15.8). ### Step 10.2 — Ships on the map - Draw in-flight ships as markers along their route lines, positioned with sim interpolation every frame, and idle ships as badges at their location. - **Test:** Unit test: marker screen position for a mocked dispatch at 50% progress lies on the route line between the two endpoints. Manual check: ships move smoothly. @@ -658,12 +668,16 @@ Steps 10H.1–10H.4 are specified in `memory-bank/change-requests/CR-008-fresh-b 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. +**Amended by CR-015:** Extended Fuel Tanks raise tank capacity (11 %/level; Courier 380 %, Freighter 9 %); the caps hold as yes/no on the fixed map. + --- ## Phase 10J — Veteran Pilot and Profit-Share Pay (CR-010) Steps 10J.1–10J.7 are specified in `memory-bank/change-requests/CR-010-veteran-pilot-profit-share.md` §5 (written there as 10G.1–10G.7), with its §6 amendments. Done after Phase 10I and before Phase 11; stop for review at the end. +**Amended by CR-015:** Pilot pay uses the leg's fuel cost (burned × the tank's average price, decision 0.5.98). + --- ## Phase 10K — Settings Screen and Company Backup (CR-011) @@ -678,6 +692,8 @@ Steps 10K.1–10K.12 are specified in `memory-bank/change-requests/CR-011-settin Steps 10L.1–10L.11 are specified in `memory-bank/change-requests/CR-012-lighter-clipper-range.md` §6 (written there as 10I.1–10I.11), with its §7 amendments. Done after Phase 10K and before Phase 11; stop for review at the end. +**Amended by CR-015:** The reach table is replaced by CR-015 §4 (yes/no); the no-strand rule uses fuel; Market and map reach lines have no dates. + --- ## Phase 10M — The Veteran's Clipper (CR-013) diff --git a/memory-bank/progress.md b/memory-bank/progress.md index da21e6f..e23268b 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -378,4 +378,13 @@ Spec: `memory-bank/change-requests/CR-015-fixed-map-fuel-tanks.md` (received as - [x] **Step 15.6 — Engine: commands and save migration.** (The save fields, the 1 → 2 migration and full tanks on new ships came in 15.4.) New commands in the route catalog: `refuelShip` (`{ shipId, amountFu? }`: fill, or buy an amount, at the local price; the ship must be idle at a location that sells fuel; refused with `insufficient-funds` unless the player can pay for all of it, `tank-full`, `no-fuel-here`, `ship-not-idle-here`), `setRefuelPolicy` (`{ shipId, policy }`), `orderEmergencyFuel` (`{ shipId }`: only when the sim offers it, else `emergency-fuel-unavailable`; adds the fuel at 2.5 × the local price into the tank's average and the cost to the loan). Tests (`packages/engine/test/fuel.test.ts`): an older (version-1) save imported through the engine has every ship full on Auto; refuel of an amount charges the local price and keeps the weighted average, fill tops up to capacity, a full tank is refused; refuel with no credits fails and changes nothing; the policy toggles; emergency fuel is refused when not needed, and when needed adds exactly the shortfall and raises the loan by exactly its cost, credits untouched. A new ship arriving full is tested since 15.4. - [x] **Step 15.7 — Client: fuel in the UI.** `src/components/Fuel.tsx`: `FuelGauge` (a meter from empty to capacity, "N fu · P%", the refuel policy), `ShipFuel` sheet (price here; Fill with its cost; buy an amount; Auto top-up / Manual; "Emergency fuel (added to loan)" with its amount and cost, shown only when the sim offers it), `FuelPrice` (a location's price, the cheapest marked "cheapest" in green), `refuelForLeg` ("Refuel here first" buys a full tank when the credits cover it, otherwise just the leg's shortfall). Fleet: every ship card shows the gauge and a Fuel button beside Upgrades. Dispatch sheet: every pair shows "Fuel needed · on board · left after"; the cost cell is "Fuel used (at your average price)"; a pair blocked only by fuel (the sim's last check) gets "Refuel here first", which refuels, keeps the sheet open and picks the pair once flyable. Deadhead picker: fuel on board, per destination "Needs N fu · left after M fu", and "Refuel here first" when any destination is short only of fuel. Locations screen and map panel: a Fuel line per location. Delivery summaries: "Topped up N fu at X, C cr" for Auto top-ups; deadheads say "fuel used". Tests (`apps/web/test/fuel-ui.test.tsx`): the gauge's values and text match the ship; "Refuel here first" fills the tank and enables and picks the pair with the sheet still open; it buys just the shortfall when a fill is unaffordable; the fuel-used cell matches the sim at the tank's average price; Jupiter's fuel is marked cheapest and Earth's isn't; emergency fuel shows (with the sim's cost) only for a stranded, broke ship. - [x] **Step 15.8 — Client: fixed map.** The map works out each body's position and orbit ring once (`BODIES` in `src/map/scene.ts`); each frame only flights move along their fixed lines. Orbit rings stay as decoration (none for the Moon). Reach wording without dates: the Market says "From Earth: Moon, Mars · By hopping: Ceres Station" (`shipReach` returns `fromOrigins` and `byHopping`; `otherDates` is gone), the map panel says "No ship can reach it from your bases.", and the dispatch sheet's "Out of range right now (…)" is "Out of range (…)". The browser tests' `nextMarsWindow` date search is gone. Tests (`apps/web/test/fixed-map.test.tsx`): every body's on-screen centre is identical a minute, a month and 400 days on; orbit rings for every body but the Moon; the Market reach line is exactly "From Earth: Moon, Mars · By hopping: Ceres Station" with no date words; the map lists "From Earth: Clipper, Freighter, Bulk Hauler" for Mars; an out-of-range pair names the ship's reach without a date. -- [ ] Step 15.9 — Plan amendments and regression +- [x] **Step 15.9 — Plan amendments and regression.** `implementation-plan.md`: §0.3 now defers fuel price drift, depot shortages and HQ fuel storage (fixed prices are in); decisions 0.5.9, 0.5.11, 0.5.12, 0.5.25, 0.5.68, 0.5.78, 0.5.79, 0.5.81 and 0.5.84 carry CR-015 notes; Steps 2.1, 2.2, 3.3, 3.4, 3.6, 3.12, 9.5, 9.6, 9.9 and 10.1 and Phases 10I (CR-009), 10J (CR-010) and 10L (CR-012) are amended. (Decisions 0.5.93–0.5.99 were added when CR-015 was recorded; `tech-stack.md` dropped `astronomy-engine` in 15.1.) The balance simulator now also flags any moment a ship would need emergency fuel while the player has credits on Auto top-up; `balance.test.ts` asserts it never happens in any world. Regression: typecheck, lint, 1,118 unit tests (coverage 95.8 % of sim lines), 9 browser tests; balance medians: deal 2.9 min, first Market purchase 63.9, first discovery 48.9, loan paid off 135.7, four ships 135.7 (both policies). + + **Manual check for the user** (CR-015 §7 Step 14.9): + 1. Get a Freighter (Market, or Settings → Developer → add credits) and fuel it at Earth. + 2. Fleet → Fuel for the Freighter → set **Manual refuel**. + 3. Fly it to Ceres, then on to the Jupiter Cloud Platform (discover them with contracts first, or with Developer → discover all). + 4. At Jupiter: Fuel → **Fill** (6 cr/fu, the cheapest; Locations marks it). + 5. Fly back toward Earth. On the next legs the dispatch sheet's "Fuel used (at your average price)" is lower than the same leg priced at Earth's 16 cr/fu, and the Fuel sheet's average price has dropped. + +**Phase 15 complete. Stopped for the user's review.** diff --git a/packages/sim/balance/simulate.ts b/packages/sim/balance/simulate.ts index dee0b02..02e25b6 100644 --- a/packages/sim/balance/simulate.ts +++ b/packages/sim/balance/simulate.ts @@ -11,6 +11,7 @@ import { content as defaultContent, type ShipModel } from "@deadhead/content"; import { affordableRefuel, + emergencyFuel, boardPeriodIndex, fuelPriceAt, type Tank, @@ -585,6 +586,14 @@ export function simulate(options: SimulationOptions): SimulationResult { if (ship.inShopUntil !== null && ship.inShopUntil > now) continue; const stats = effectiveStats(model(ship.modelId), ship.upgrades, content); topUpHere(ship, stats.tankCapacityFu); + // CR-015 §7 (Step 15.9): on Auto top-up with money in the bank, no ship + // should ever be stuck needing emergency fuel. + if ( + credits > 0 && + emergencyFuel({ ship, credits, discoveredIds: discovered }, content) !== null + ) { + violate("needs-emergency-fuel", `${ship.id} ${ship.modelId} at ${ship.locationId}`); + } const choices = eligibleContracts(ship, pilot) .map((c) => ({ c, value: rate(c, stats, pilot) })) .filter((o) => o.value > 0) diff --git a/packages/sim/test/balance.test.ts b/packages/sim/test/balance.test.ts index cd8b173..626737f 100644 --- a/packages/sim/test/balance.test.ts +++ b/packages/sim/test/balance.test.ts @@ -66,6 +66,13 @@ describe("balance simulation (Step 12.1)", () => { } }); + it("never needs emergency fuel on Auto top-up with positive credits (CR-015, Step 15.9)", () => { + for (const result of [...lighterFirst, ...freighterFirst]) { + const stuck = result.violations.filter((v) => v.rule === "needs-emergency-fuel"); + expect(stuck, `${result.policy}`).toEqual([]); + } + }); + it("has the veteran fly at least one contract in the first session (CR-010)", () => { for (const result of lighterFirst) expect(result.veteranContractsFirstSession).toBeGreaterThan(0);