From 10ded07ea696ad1c82037038f612b91143edd300 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Fri, 2 Oct 2026 17:42:52 -0400 Subject: [PATCH] Step 2.3: add the three ship models Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 6 ++++ memory-bank/progress.md | 2 +- packages/content/src/data/ships.ts | 51 +++++++++++++++++++++++++++++ packages/content/src/index.ts | 1 + packages/content/test/ships.test.ts | 42 ++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/content/src/data/ships.ts create mode 100644 packages/content/test/ships.test.ts diff --git a/DECISIONS.md b/DECISIONS.md index 3d075d6..311eb74 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -51,3 +51,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Launch multipliers:** Earth 3, Jupiter Cloud Platform 2, Mars 1.5, Moon 0.5, Ceres 0.3. - **Bay lease prices are first guesses** (Earth 5,000 up to Jupiter 15,000). Step 2.5 tunes the Earth price against the tutorial. - **The cargo-id check uses a placeholder list** of the four cargo ids planned in Step 2.4 until the cargo data exists. + +## Step 2.3 + +- **Ranges** (million km): Courier 5 (Moon only; Mars is ~55 at its closest), Freighter 650 (covers Ceres at its farthest, ~600, and Jupiter near its closest, ~590, per decision 0.5.25), Bulk Hauler 1,000 (Jupiter at its farthest, ~970). Step 3.7 checks these against real positions. +- **Speeds and overheads** aim at GDD §6 durations at compression 60: Courier 3M km/h with 2 h overhead (Earth→Moon ≈ 2 real minutes, mostly overhead), Freighter 2M km/h with 4 h (Earth→Mars ≈ 1–2 real hours), Bulk Hauler 1.2M km/h with 8 h. +- **Prices** (30,000 / 150,000 / 600,000), **capacities** (20 / 100 / 400 t), **fuel costs** (20 / 40 / 80 per million km) and **hull ratings** (1 / 2 / 3) are first guesses; Step 2.5 tunes the Courier price. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 17628ff..921407d 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -12,7 +12,7 @@ - [x] **Step 2.1 — Content schemas.** Zod 4 schemas in `packages/content/src/schemas.ts` for Location, ShipModel, CargoType, Skill and GameConfig, with inferred types. Tests check that each schema accepts a valid example and rejects it with any one field missing. - [x] **Step 2.2 — The five locations.** `src/data/locations.ts`: Earth, Moon, Mars, Ceres Station, Jupiter Cloud Platform. Earth and Moon start discovered; Earth has the highest launch multiplier. -- [ ] Step 2.3 — The three ship models +- [x] **Step 2.3 — The three ship models.** `src/data/ships.ts`: Courier, Freighter, Bulk Hauler, all atmospheric. Speed, capacity and price orderings tested; range rules are checked in Step 3.7. - [ ] Step 2.4 — Cargo types and skills - [ ] Step 2.5 — Default game config - [ ] Step 2.6 — Single content bundle diff --git a/packages/content/src/data/ships.ts b/packages/content/src/data/ships.ts new file mode 100644 index 0000000..75a50b4 --- /dev/null +++ b/packages/content/src/data/ships.ts @@ -0,0 +1,51 @@ +import type { ShipModel } from "../schemas"; + +/** + * Base-game ship models (GDD §8). All are atmospheric-rated (decision 0.5.1). + * Ranges against approximate Earth distances: Moon ~0.4M km, Mars at closest + * ~55M km, Ceres at farthest ~600M km, Jupiter ~590M–970M km. + */ +export const shipModels: ShipModel[] = [ + { + id: "courier", + name: "Courier", + archetype: "courier", + price: 30000, + cruiseSpeedKmPerGameHour: 3_000_000, + cargoCapacityTonnes: 20, + // Moon only. + fuelRangeMillionKm: 5, + hullRating: 1, + landingClass: "atmospheric", + overheadGameHours: 2, + fuelCostPerMillionKm: 20, + }, + { + id: "freighter", + name: "Freighter", + archetype: "freighter", + price: 150000, + cruiseSpeedKmPerGameHour: 2_000_000, + cargoCapacityTonnes: 100, + // Mars and Ceres at any distance; Jupiter only near its closest (decision 0.5.25). + fuelRangeMillionKm: 650, + hullRating: 2, + landingClass: "atmospheric", + overheadGameHours: 4, + fuelCostPerMillionKm: 40, + }, + { + id: "bulk-hauler", + name: "Bulk Hauler", + archetype: "bulk-hauler", + price: 600000, + cruiseSpeedKmPerGameHour: 1_200_000, + cargoCapacityTonnes: 400, + // Jupiter at its farthest. + fuelRangeMillionKm: 1000, + hullRating: 3, + landingClass: "atmospheric", + overheadGameHours: 8, + fuelCostPerMillionKm: 80, + }, +]; diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 8ea10f7..c817643 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -1,2 +1,3 @@ export * from "./schemas"; export * from "./data/locations"; +export * from "./data/ships"; diff --git a/packages/content/test/ships.test.ts b/packages/content/test/ships.test.ts new file mode 100644 index 0000000..bcd5191 --- /dev/null +++ b/packages/content/test/ships.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { shipModelSchema, shipModels } from "../src/index"; + +const ship = (id: string) => shipModels.find((s) => s.id === id)!; + +describe("ship models", () => { + it("all pass the schema", () => { + for (const model of shipModels) { + expect(() => shipModelSchema.parse(model)).not.toThrow(); + } + }); + + it("has the three archetypes, all atmospheric", () => { + expect(shipModels.map((s) => s.id)).toEqual(["courier", "freighter", "bulk-hauler"]); + for (const model of shipModels) { + expect(model.landingClass).toBe("atmospheric"); + } + }); + + it("orders speed Courier > Freighter > Bulk Hauler", () => { + expect(ship("courier").cruiseSpeedKmPerGameHour).toBeGreaterThan( + ship("freighter").cruiseSpeedKmPerGameHour, + ); + expect(ship("freighter").cruiseSpeedKmPerGameHour).toBeGreaterThan( + ship("bulk-hauler").cruiseSpeedKmPerGameHour, + ); + }); + + it("orders capacity Bulk Hauler > Freighter > Courier", () => { + expect(ship("bulk-hauler").cargoCapacityTonnes).toBeGreaterThan( + ship("freighter").cargoCapacityTonnes, + ); + expect(ship("freighter").cargoCapacityTonnes).toBeGreaterThan( + ship("courier").cargoCapacityTonnes, + ); + }); + + it("orders price Courier < Freighter < Bulk Hauler", () => { + expect(ship("courier").price).toBeLessThan(ship("freighter").price); + expect(ship("freighter").price).toBeLessThan(ship("bulk-hauler").price); + }); +});