Step 2.4: add cargo types and skills

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-02 17:43:32 -04:00
parent 10ded07ea6
commit 6804a7120c
6 changed files with 125 additions and 4 deletions

View File

@ -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.

View File

@ -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

View File

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

View File

@ -1,3 +1,4 @@
export * from "./schemas";
export * from "./data/locations";
export * from "./data/ships";
export * from "./data/cargo";

View File

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

View File

@ -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", () => {