diff --git a/DECISIONS.md b/DECISIONS.md index 311eb74..427a9d4 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -57,3 +57,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **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. + +## Step 2.4 + +- **Ids:** cargo `standard`, `heavy`, `high-value`, `dangerous`; skills `long-haul`, `heavy-cargo`, `expensive-cargo`, `dangerous-cargo`. +- **Base pay** (credits per tonne per million km): Standard 10, Heavy 14, High-Value 18, Dangerous 20. **Tier multipliers:** 1, 1.2, 1.25, 1.3. First guesses, to be tuned in Phase 12. +- **Cargo and skills share one data file** (`src/data/cargo.ts`), since every cargo skill exists to gate a cargo type. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 921407d..a9df07a 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -13,6 +13,6 @@ - [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. - [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 +- [x] **Step 2.4 — Cargo types and skills.** `src/data/cargo.ts`: Standard, Heavy, High-Value, Dangerous; skills Long Haul, Heavy Cargo, Expensive Cargo, Dangerous Cargo (max level 5). The location test now checks cargo ids against the real data. - [ ] Step 2.5 — Default game config - [ ] Step 2.6 — Single content bundle diff --git a/packages/content/src/data/cargo.ts b/packages/content/src/data/cargo.ts new file mode 100644 index 0000000..7142a77 --- /dev/null +++ b/packages/content/src/data/cargo.ts @@ -0,0 +1,60 @@ +import type { CargoType, Skill } from "../schemas"; + +/** Standard is always tier 0; other types carry a tier from 1 to 5 (decision 0.5.6). */ +export const cargoTypes: CargoType[] = [ + { + id: "standard", + name: "Standard", + basePayPerTonnePerMillionKm: 10, + tierPayMultiplier: 1, + requiredSkillId: null, + }, + { + id: "heavy", + name: "Heavy", + basePayPerTonnePerMillionKm: 14, + tierPayMultiplier: 1.2, + requiredSkillId: "heavy-cargo", + }, + { + id: "high-value", + name: "High-Value", + basePayPerTonnePerMillionKm: 18, + tierPayMultiplier: 1.25, + requiredSkillId: "expensive-cargo", + }, + { + id: "dangerous", + name: "Dangerous", + basePayPerTonnePerMillionKm: 20, + tierPayMultiplier: 1.3, + requiredSkillId: "dangerous-cargo", + }, +]; + +export const skills: Skill[] = [ + { + id: "long-haul", + name: "Long Haul", + maxLevel: 5, + description: "Access to longer routes.", + }, + { + id: "heavy-cargo", + name: "Heavy Cargo", + maxLevel: 5, + description: "Access to heavier loads, one tier per level.", + }, + { + id: "expensive-cargo", + name: "Expensive Cargo", + maxLevel: 5, + description: "Access to high-value cargo, one tier per level.", + }, + { + id: "dangerous-cargo", + name: "Dangerous Cargo", + maxLevel: 5, + description: "Access to hazardous cargo, one hazard class per level.", + }, +]; diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index c817643..3d0b1c1 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -1,3 +1,4 @@ export * from "./schemas"; export * from "./data/locations"; export * from "./data/ships"; +export * from "./data/cargo"; diff --git a/packages/content/test/cargo.test.ts b/packages/content/test/cargo.test.ts new file mode 100644 index 0000000..aed668b --- /dev/null +++ b/packages/content/test/cargo.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { cargoTypeSchema, cargoTypes, skillSchema, skills } from "../src/index"; + +describe("cargo types", () => { + it("all pass the schema", () => { + for (const cargo of cargoTypes) { + expect(() => cargoTypeSchema.parse(cargo)).not.toThrow(); + } + }); + + it("has Standard plus three skill-gated types", () => { + expect(cargoTypes.map((c) => c.id)).toEqual(["standard", "heavy", "high-value", "dangerous"]); + expect(cargoTypes.find((c) => c.id === "standard")!.requiredSkillId).toBeNull(); + }); + + it("requires only skills that exist", () => { + const skillIds = skills.map((s) => s.id); + for (const cargo of cargoTypes) { + if (cargo.requiredSkillId !== null) { + expect(skillIds).toContain(cargo.requiredSkillId); + } + } + }); + + it("pays more than Standard for every non-standard type", () => { + const standard = cargoTypes.find((c) => c.id === "standard")!; + for (const cargo of cargoTypes) { + if (cargo !== standard) { + expect(cargo.basePayPerTonnePerMillionKm).toBeGreaterThan( + standard.basePayPerTonnePerMillionKm, + ); + } + } + }); +}); + +describe("skills", () => { + it("all pass the schema", () => { + for (const skill of skills) { + expect(() => skillSchema.parse(skill)).not.toThrow(); + } + }); + + it("has the four base skills, each max level 5", () => { + expect(skills.map((s) => s.id)).toEqual([ + "long-haul", + "heavy-cargo", + "expensive-cargo", + "dangerous-cargo", + ]); + for (const skill of skills) { + expect(skill.maxLevel).toBe(5); + } + }); +}); diff --git a/packages/content/test/locations.test.ts b/packages/content/test/locations.test.ts index 6d1e50d..075700d 100644 --- a/packages/content/test/locations.test.ts +++ b/packages/content/test/locations.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it } from "vitest"; -import { locationSchema, locations } from "../src/index"; +import { cargoTypes, 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"]; +const cargoTypeIds = cargoTypes.map((c) => c.id); describe("locations", () => { it("all pass the schema", () => {