Step 2.3: add the three ship models

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-02 17:42:52 -04:00
parent 24d9942336
commit 10ded07ea6
5 changed files with 101 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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