From 24d9942336318a4b1b48437a7e281ef27f496f1a Mon Sep 17 00:00:00 2001 From: fireshaper Date: Fri, 2 Oct 2026 17:42:07 -0400 Subject: [PATCH] Step 2.2: add the five locations Also fixes Prettier formatting in DECISIONS.md from Step 2.1. Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 10 +++- memory-bank/progress.md | 2 +- packages/content/src/data/locations.ts | 69 +++++++++++++++++++++++++ packages/content/src/index.ts | 1 + packages/content/test/locations.test.ts | 40 ++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 packages/content/src/data/locations.ts create mode 100644 packages/content/test/locations.test.ts diff --git a/DECISIONS.md b/DECISIONS.md index 013b26d..3d075d6 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -40,6 +40,14 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Epoch fields are ISO 8601 UTC strings** (`epoch.realStart`, `epoch.ephemerisDate`), not `Date` objects, so content stays plain data. - **`tutorialBoardSize`** is a GameConfig field in addition to `contractBoardSize`, because decision 0.5.21 sets the tutorial board's size separately. - **XP per delivery inputs** are `base`, `perMillionKm` and `perCargoTier`. Step 3.16 decides how they combine. -- **`pilotLevelXpThresholds`** lists the *total* XP needed for level 1, 2, 3, ...; it and the Long Haul cut-offs must be strictly increasing. +- **`pilotLevelXpThresholds`** lists the _total_ XP needed for level 1, 2, 3, ...; it and the Long Haul cut-offs must be strictly increasing. - **`tierPayMultiplier`** is one number per cargo type. Step 3.8 decides how it scales with tier. - **Imports omit the `.ts` extension** (`moduleResolution: "Bundler"`). + +## Step 2.2 + +- **Location ids:** `earth`, `moon`, `mars`, `ceres-station`, `jupiter-cloud-platform`. +- **Cargo flows** follow GDD §9: Earth and Mars produce standard and high-value goods and demand ore (heavy) and fuel (dangerous); the Moon produces standard and heavy; Ceres produces heavy; Jupiter produces dangerous (fuel). The outposts import high-value equipment. +- **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. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 9e6327c..17628ff 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -11,7 +11,7 @@ ## Phase 2 — Content Package - [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. -- [ ] Step 2.2 — The five locations +- [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 - [ ] Step 2.4 — Cargo types and skills - [ ] Step 2.5 — Default game config diff --git a/packages/content/src/data/locations.ts b/packages/content/src/data/locations.ts new file mode 100644 index 0000000..275d04e --- /dev/null +++ b/packages/content/src/data/locations.ts @@ -0,0 +1,69 @@ +import type { Location } from "../schemas"; + +/** + * Base-game destinations (GDD §9). Cargo flows: Earth and Mars make goods, the + * Moon and Ceres mine ore (heavy), Jupiter refines fuel (dangerous); remote + * outposts import high-value equipment. + */ +export const locations: Location[] = [ + { + id: "earth", + name: "Earth", + type: "atmospheric-world", + landingRequirement: "atmospheric", + body: "earth", + launchCostMultiplier: 3, + startsDiscovered: true, + bayLeasePrice: 5000, + produces: ["standard", "high-value"], + demands: ["heavy", "dangerous"], + }, + { + id: "moon", + name: "Moon", + type: "airless-body", + landingRequirement: "vacuum", + body: "moon", + launchCostMultiplier: 0.5, + startsDiscovered: true, + bayLeasePrice: 3000, + produces: ["standard", "heavy"], + demands: ["standard", "high-value", "dangerous"], + }, + { + id: "mars", + name: "Mars", + type: "atmospheric-world", + landingRequirement: "atmospheric", + body: "mars", + launchCostMultiplier: 1.5, + startsDiscovered: false, + bayLeasePrice: 8000, + produces: ["standard", "high-value"], + demands: ["heavy", "dangerous"], + }, + { + id: "ceres-station", + name: "Ceres Station", + type: "belt-station", + landingRequirement: "vacuum", + body: "ceres", + launchCostMultiplier: 0.3, + startsDiscovered: false, + bayLeasePrice: 10000, + produces: ["heavy"], + demands: ["standard", "high-value", "dangerous"], + }, + { + id: "jupiter-cloud-platform", + name: "Jupiter Cloud Platform", + type: "cloud-platform", + landingRequirement: "atmospheric", + body: "jupiter", + launchCostMultiplier: 2, + startsDiscovered: false, + bayLeasePrice: 15000, + produces: ["dangerous"], + demands: ["standard", "high-value", "heavy"], + }, +]; diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 292f605..8ea10f7 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -1 +1,2 @@ export * from "./schemas"; +export * from "./data/locations"; diff --git a/packages/content/test/locations.test.ts b/packages/content/test/locations.test.ts new file mode 100644 index 0000000..6d1e50d --- /dev/null +++ b/packages/content/test/locations.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { locationSchema, locations } from "../src/index"; + +// The cargo types planned for Step 2.4; replaced by the real data once it exists. +const cargoTypeIds = ["standard", "heavy", "high-value", "dangerous"]; + +describe("locations", () => { + it("all pass the schema", () => { + for (const location of locations) { + expect(() => locationSchema.parse(location)).not.toThrow(); + } + }); + + it("has exactly five locations with unique ids", () => { + expect(locations).toHaveLength(5); + expect(new Set(locations.map((l) => l.id)).size).toBe(5); + }); + + it("starts with exactly Earth and Moon discovered", () => { + const discovered = locations.filter((l) => l.startsDiscovered).map((l) => l.id); + expect(discovered.sort()).toEqual(["earth", "moon"]); + }); + + it("references only existing cargo types", () => { + for (const location of locations) { + for (const cargoId of [...location.produces, ...location.demands]) { + expect(cargoTypeIds).toContain(cargoId); + } + } + }); + + it("gives Earth the highest launch multiplier", () => { + const earth = locations.find((l) => l.id === "earth")!; + for (const location of locations) { + if (location !== earth) { + expect(earth.launchCostMultiplier).toBeGreaterThan(location.launchCostMultiplier); + } + } + }); +});