diff --git a/DECISIONS.md b/DECISIONS.md index 5d12c1f..f16a57c 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -72,3 +72,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Starting credits are 0.** The company pays fuel during the tutorial, and the ~21-run target already assumes the player starts with nothing. - **Other defaults:** board size 8, tutorial board 40 (more than the ~30 runs that fit in a 60-minute period), first contact 10% at ×2 pay, contract mass 5–400 t, Long Haul cut-offs 100/250/400/600/800M km, ten pilot levels at 2 skill points each (enough to max all four skills), hire cost 2,000, pilot pay 100 per delivery. - **Known balance issue for Phase 12:** pay is linear in distance, but every trip pays a fixed overhead, so short Earth–Moon runs earn far less per real hour than long runs (a Freighter on Mars earns roughly 50× a Courier on the Moon). + +## Step 2.6 + +- **Lookups return `undefined`** for an unknown id ("not found" without throwing). Callers decide whether a missing id is an error. +- **The package also exports each data list and schema individually**, alongside the `content` bundle, so tests and the server can validate data with the schemas. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 414ec54..f51c789 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -15,4 +15,4 @@ - [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. - [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. - [x] **Step 2.5 — Default game config.** `src/data/config.ts`. Courier + Earth bay ≈ 21 tutorial runs with the placeholder average payout. Money values rescaled ×10 so tutorial runs pay meaningful amounts; epoch moved from `.env.example` into GameConfig. -- [ ] Step 2.6 — Single content bundle +- [x] **Step 2.6 — Single content bundle.** `content` in `src/bundle.ts` holds all data plus `location`, `shipModel`, `cargoType` and `skill` lookups that return `undefined` for unknown ids. diff --git a/packages/content/src/bundle.ts b/packages/content/src/bundle.ts new file mode 100644 index 0000000..a123ea1 --- /dev/null +++ b/packages/content/src/bundle.ts @@ -0,0 +1,24 @@ +import { cargoTypes, skills } from "./data/cargo"; +import { gameConfig } from "./data/config"; +import { locations } from "./data/locations"; +import { shipModels } from "./data/ships"; + +/** Returns a lookup that gives `undefined` for an unknown id instead of throwing. */ +function byId(items: readonly T[]): (id: string) => T | undefined { + const map = new Map(items.map((item) => [item.id, item])); + return (id) => map.get(id); +} + +/** All static game data in one object, with lookups by id. */ +export const content = { + locations, + shipModels, + cargoTypes, + skills, + config: gameConfig, + location: byId(locations), + shipModel: byId(shipModels), + cargoType: byId(cargoTypes), + skill: byId(skills), +}; +export type Content = typeof content; diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 4f0f98b..469ffa1 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -3,3 +3,4 @@ export * from "./data/locations"; export * from "./data/ships"; export * from "./data/cargo"; export * from "./data/config"; +export * from "./bundle"; diff --git a/packages/content/test/bundle.test.ts b/packages/content/test/bundle.test.ts new file mode 100644 index 0000000..c0d5740 --- /dev/null +++ b/packages/content/test/bundle.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { content } from "../src/index"; + +describe("content bundle", () => { + it("contains every kind of content", () => { + expect(content.locations).toHaveLength(5); + expect(content.shipModels).toHaveLength(3); + expect(content.cargoTypes).toHaveLength(4); + expect(content.skills).toHaveLength(4); + expect(content.config.timeCompression).toBe(60); + }); + + it("looks up one item of each kind by id", () => { + expect(content.location("mars")?.name).toBe("Mars"); + expect(content.shipModel("freighter")?.name).toBe("Freighter"); + expect(content.cargoType("dangerous")?.name).toBe("Dangerous"); + expect(content.skill("long-haul")?.name).toBe("Long Haul"); + }); + + it("returns undefined for an unknown id without throwing", () => { + expect(content.location("pluto")).toBeUndefined(); + expect(content.shipModel("x-wing")).toBeUndefined(); + expect(content.cargoType("cheese")).toBeUndefined(); + expect(content.skill("juggling")).toBeUndefined(); + }); +});