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 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-02 17:42:07 -04:00
parent 8ff2df976e
commit 24d9942336
5 changed files with 120 additions and 2 deletions

View File

@ -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. - **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. - **`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. - **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. - **`tierPayMultiplier`** is one number per cargo type. Step 3.8 decides how it scales with tier.
- **Imports omit the `.ts` extension** (`moduleResolution: "Bundler"`). - **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.

View File

@ -11,7 +11,7 @@
## Phase 2 — Content Package ## 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. - [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.3 — The three ship models
- [ ] Step 2.4 — Cargo types and skills - [ ] Step 2.4 — Cargo types and skills
- [ ] Step 2.5 — Default game config - [ ] Step 2.5 — Default game config

View File

@ -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"],
},
];

View File

@ -1 +1,2 @@
export * from "./schemas"; export * from "./schemas";
export * from "./data/locations";

View File

@ -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);
}
}
});
});