diff --git a/DECISIONS.md b/DECISIONS.md index 3c6182e..3fc3639 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1516,3 +1516,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` ## Step 16.3 - **No content changed**: the Phase 15 tank sizes already satisfy CR-016 §3 with margins (the closest call is a Freighter from Mars to Io, ≈ 529M km of reach for ≈ 592). The step adds the tests only. + +## Step 16.4 + +- **No save migration for new locations**: CR-016 §9 asks for one that adds the three places as undiscovered, but the save (since CR-014) stores only discovered places, so there is nothing to add. Bumping the save version for no data change would only make older builds refuse newer saves for no reason; the test shows an older save opens correctly instead. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index e797199..0cff7d1 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -396,7 +396,7 @@ Spec: `memory-bank/change-requests/CR-016-venus-io-europa-belt.md` (received as - [x] **Step 16.1 — Content: new locations.** Venus Cloud City (cloud platform, atmospheric, 108M km at 110°, range factor 1.0, fuel × 1.3, bay 20,000), Io Sulfur Works (new type `orbital-station`, `vacuum` landing so any ship can land, 0.42M km out from Jupiter, factor 1.2, fuel × 1.0, bay 70,000) and Europa Ice Station (new type `surface-base`, `vacuum`, 0.67M km out from Jupiter, factor 1.3, fuel × 0.7, bay 70,000); all undiscovered, selling fuel, with the CR's shippers and class mixes. Pulled forward so the game keeps working: each new place's Standard good (Cloud-Grown Botanicals at Venus, a new good; Mail & Parcels also produced at Io and Europa), since boards can't be generated where nothing Standard is made; and the three body sprites in the art manifest, with their generated art (the belt is still 16.5). The balance check's first Market purchase moved from 64 to ~92 min (the Clipper's landings decide it, and Venus changed which first contacts boards offer); the user widened the window to 45–95. Tests: `packages/content/test/fixed-map.test.ts` (Earth/Moon–Venus ≈ 200, Venus–Mars ≈ 332, Venus–Ceres ≈ 520, Venus–Jupiter ≈ 885, Ceres–Io/Europa ≈ 393, all within 2 %; the Jupiter system within 1.1M km; Io and Europa ride with Jupiter at their offsets; the three undiscovered, selling fuel, not Hardened; only Earth and the Moon start discovered); `locations.test.ts` (eight locations; fuel prices ordered Jupiter < Europa < Ceres < Io < Moon < Venus < Mars < Earth). Updated: bundle count, first contacts may go to Mars or Venus, the CR-007 class-mix baseline measured on its original five places, orbit rings for every body but those riding a parent, the Market reach line now listing Venus. - [x] **Step 16.2 — Content: goods.** New goods: Atmospheric Data Cores (High-Value tier 2, Venus → Earth), Refined Sulfur (Heavy, Io → Earth, Mars, Venus), Volcanic Isotopes (High-Value tier 3, Io → Earth), Subsurface Ocean Samples (High-Value tier 3, Priority, Europa → Earth); Cloud-Grown Botanicals came in 16.1. Water Ice and Liquid Oxygen are also made at Europa, Industrial Chemicals at Venus; Water Ice goes to Io and Venus, Liquid Oxygen to Io, Habitat Prefabs to Io and Europa, and Packaged Food, Consumer Goods, Mail & Parcels, Medical Supplies, Electronics and Machine Parts to all three new places. Balance medians: first purchase 92.3, discovery 48.9, loan 160.5, four ships 147 vs 156. Tests (`packages/content/test/goods.test.ts`): the five new goods with their classes, tiers and routes (Sulfur has no tier of its own); every location makes a Standard good and wants at least one; each new place is a destination for at least five goods; the new producers on existing goods. - [x] **Step 16.3 — Content tuning: reach.** No tuning needed: with Phase 15's tanks every cell of CR-015 §4 and of CR-016 §3 already holds (Clipper ≈ 252M km reaches Venus at ≈ 200 but not Mars from Venus at ≈ 332; Freighter ≈ 460 reaches Mars from Venus and Io/Europa from Ceres at ≈ 393 but not Ceres from Venus at ≈ 520, nor Io from Mars at ≈ 592 × 1.15 short; Bulk Hauler ≈ 1,251 covers Venus–Jupiter at ≈ 885). Tests (`reach-table.test.ts`, CR-016 §3): one test per new cell (the Jupiter-system loop for Courier, Lighter, Clipper and Freighter; Earth↔Venus and Moon→Venus for Clipper and Freighter, not Courier or Lighter; Venus→Mars only from the Freighter up; Ceres↔Io/Europa for the Freighter; nothing from the inner system to the Jupiter system short of the Bulk Hauler, which flies every leg between all eight); the Bulk Hauler's full-tank reach ≥ 900M km; a Courier at the platform flies to Io and Europa and back without stranding. The no-strand test already covers every leg between all eight places. -- [ ] Step 16.4 — Engine: save migration and boards +- [x] **Step 16.4 — Engine: save migration and boards.** No save migration and no save-version change: the save lists only discovered places, so a place that is in content but not in that list is undiscovered, and a save from before CR-016 opens with the three new places undiscovered and nothing else changed. Boards needed no change either: first contacts and fit slots already draw from undiscovered places in reach. Tests (`packages/engine/test/new-places.test.ts`): a pre-CR-016 save imported through the engine shows eight places with Venus, Io and Europa undiscovered, and its save version, discoveries and company are unchanged; 200 Earth boards for the deal's Clipper and veteran include first contacts to Venus; a Clipper delivering a first contact to Venus discovers it (the logbook says so) and Venus then has its own board, every contract on it a good made at Venus. - [ ] Step 16.5 — Assets: sprites and belt rocks - [ ] Step 16.6 — Map: belt haze - [ ] Step 16.7 — Map: belt rocks diff --git a/packages/engine/test/new-places.test.ts b/packages/engine/test/new-places.test.ts new file mode 100644 index 0000000..9b78723 --- /dev/null +++ b/packages/engine/test/new-places.test.ts @@ -0,0 +1,100 @@ +import { content } from "@deadhead/content"; +import { boardPeriodIndex, generateContractBoard } from "@deadhead/sim"; +import { describe, expect, it } from "vitest"; +import { fullSave } from "../../schema/test/save-fixture"; +import { deliveredTank } from "../src/game/state"; +import { encodeSave } from "../src/index"; +import { + edit, + getState, + putBoard, + readSave, + setCredits, + signUpOwner, + TEST_NOW, + testContract, + useTestApp, +} from "./support"; + +const testApp = useTestApp(); +const NEW_PLACES = ["venus-cloud-city", "io-sulfur-works", "europa-ice-station"]; + +describe("Venus, Io and Europa in the engine (Step 16.4)", () => { + it("opens a save from before CR-016 with eight places, the new three undiscovered", async () => { + const { app } = testApp(); + const before = fullSave(); + const { state } = await app.call("importSave", { code: await encodeSave(before) }); + expect(state.locations).toHaveLength(8); + for (const id of NEW_PLACES) { + expect( + state.locations.find((l) => l.id === id), + id, + ).toEqual({ id, discovered: false }); + } + // Nothing else about what the player knows changed: no save data is needed for them. + const after = await readSave(app); + expect(after.saveVersion).toBe(before.saveVersion); + expect(after.discoveredLocations).toEqual(before.discoveredLocations); + expect(after.company).toEqual(before.company); + }); + + it("offers first contacts to Venus on Earth boards over time", () => { + const start = boardPeriodIndex(TEST_NOW, content.config); + const toVenus = Array.from({ length: 200 }, (_, k) => + generateContractBoard( + { + location: content.location("earth")!, + discoveredIds: new Set(["earth", "moon"]), + periodIndex: start + k, + worldSeed: 4242, + // The deal's Clipper and veteran, after Long Haul training. + fleet: [{ modelId: "clipper", skills: { "expensive-cargo": 1, "long-haul": 2 } }], + }, + content, + ), + ) + .flat() + .filter((c) => c.firstContact && c.destinationId === "venus-cloud-city"); + expect(toVenus.length).toBeGreaterThan(0); + }); + + it("discovers Venus on delivery and opens its board with Venus goods", async () => { + const { app, clock } = testApp(); + const owner = await signUpOwner(app); + await setCredits(app, 10_000); + // A Clipper with a full tank: Venus is a Clipper leg from Earth (CR-016 §3). + await edit(app, (save) => { + Object.assign( + save.ships.find((s) => s.id === owner.shipId)!, + { + modelId: "clipper", + ...deliveredTank("clipper", "earth"), + }, + ); + }); + const contract = testContract({ + destinationId: "venus-cloud-city", + firstContact: true, + payout: 40_000, + }); + await putBoard(app, [contract]); + const { dispatch } = await app.call("createDispatch", { + kind: "contract", + contractId: contract.id, + shipId: owner.shipId, + pilotId: owner.avatarId, + }); + + clock.now = dispatch.arrivalAt; + const state = await getState(app); + expect(state.locations.find((l) => l.id === "venus-cloud-city")!.discovered).toBe(true); + expect(state.summaries[0]!.discoveredLocationId).toBe("venus-cloud-city"); + + const { contracts } = await app.call("getContractBoard", { locationId: "venus-cloud-city" }); + expect(contracts.length).toBeGreaterThan(0); + for (const c of contracts) { + expect(c.originId).toBe("venus-cloud-city"); + expect(content.good(c.goodId)!.producedAt, c.goodId).toContain("venus-cloud-city"); + } + }); +});