diff --git a/DECISIONS.md b/DECISIONS.md index b1bd477..4c5641f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1669,3 +1669,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Numbering:** received as "CR-019 — Independent Pilots" for "Phase 19"; stored as **CR-020**, built as **Phase 20** (its 19.n = 20.n). Its decisions CR-019.D1–D9 are **0.5.118–0.5.126**. Its references: CR-005 → CR-007, CR-007 → CR-010, CR-011 → CR-014 (its 10K.6 → Step 14.6), CR-014 → CR-015, CR-016 → CR-017, CR-017 → CR-018, CR-018 → CR-019; CR-001 and CR-004 are unchanged. - **Plan 0.3:** automation/standing routes is struck from the deferred list; standing orders, pilot traits, decision events and a hiring market stay deferred. + +## Step 20.1 + +- **The settings live in one `independentPilots` group** in GameConfig. +- **The Dispatch Office costs exactly a Lighter (36,000 cr)**, the CR's "start around the price of a Lighter", until Step 20.8's balance pass says otherwise. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 1fc43b7..fcf0873 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -462,7 +462,7 @@ Spec: `memory-bank/change-requests/CR-020-bridge-dashboard.md`. New decisions re Spec: `memory-bank/change-requests/CR-020-independent-pilots.md` (received as "CR-019 … Phase 19"). New decisions recorded as plan §0.5.118–0.5.126. -- [ ] Step 20.1 — Content and config +- [x] **Step 20.1 — Content and config.** `config.independentPilots`: `dispatchOfficePrice` 36,000 (a Lighter), `firstLookRealMinutes` 3, `turnaroundGameMinutes` 15 (15 real seconds at ×60), `emptyPeriodsBeforeHome` 3, `maxCatchUpRealDays` 7 and `defaultSpendingFloor` 500. All must be positive, and the first-look window must be shorter than the board period. Two stale quarter-tank comments from CR-015 are gone. Tests (`packages/content/test/config.test.ts`): the defaults, with the Dispatch Office at the Lighter's price; zero and negative values rejected; a first-look window as long as the board period rejected. - [ ] Step 20.2 — Sim: eligibility and job choice - [ ] Step 20.3 — Engine: Dispatch Office, modes, orders - [ ] Step 20.4 — Engine: event-queue catch-up diff --git a/packages/content/src/data/config.ts b/packages/content/src/data/config.ts index c0d9742..ba4eaca 100644 --- a/packages/content/src/data/config.ts +++ b/packages/content/src/data/config.ts @@ -25,8 +25,7 @@ export const gameConfig: GameConfig = { loanRepaymentShare: 0.25, // The deal (CR-013): Halvorsen's Courier for the avatar, his Clipper for the // veteran, and the Earth bay. The loan is computed from their prices. - // The ships come with a quarter tank (CR-015, user decision): the Clipper earns - // locally before the player can fuel a Mars run, as before fuel tanks. + // The deal's ships come with full tanks (CR-018, decision 0.5.111). deal: { avatarShipModelId: "courier", veteranShipModelId: "clipper", @@ -87,4 +86,15 @@ export const gameConfig: GameConfig = { pilotHireCost: 2000, // A quarter of each contract's profit (payout − fuel) goes to a hired pilot (CR-010). pilotProfitShare: 0.25, + // Independent pilots (CR-020): the Dispatch Office costs about a Lighter; the + // player has the first 3 minutes of every 10-minute board; pilots turn round + // in 15 game minutes (15 real seconds) and head home after 3 empty boards. + independentPilots: { + dispatchOfficePrice: 36_000, + firstLookRealMinutes: 3, + turnaroundGameMinutes: 15, + emptyPeriodsBeforeHome: 3, + maxCatchUpRealDays: 7, + defaultSpendingFloor: 500, + }, }; diff --git a/packages/content/src/schemas.ts b/packages/content/src/schemas.ts index 3ea49e4..5b50548 100644 --- a/packages/content/src/schemas.ts +++ b/packages/content/src/schemas.ts @@ -242,7 +242,6 @@ export const gameConfigSchema = z avatarShipModelId: z.string().min(1), veteranShipModelId: z.string().min(1), bayLocationId: z.string().min(1), - /** How full the deal's ships' tanks are (CR-015, user decision): Halvorsen's leftovers. */ }), contractBoardPeriodRealMinutes: z.number().positive(), /** Past board periods whose boards and accepted contracts are kept (Step 7.3). */ @@ -330,12 +329,35 @@ export const gameConfigSchema = z pilotHireCost: nonNegativeInt, /** Hired pilots' share of profit (payout − fuel) per contract delivery (decision 0.5.12, CR-010). */ pilotProfitShare: fraction, + /** Independent pilots (CR-020, decisions 0.5.119–0.5.125). */ + independentPilots: z.object({ + /** Credits: the one-time Dispatch Office (0.5.119). */ + dispatchOfficePrice: positiveInt, + /** Real minutes into each board period that its jobs are the player's alone (0.5.122). */ + firstLookRealMinutes: z.number().positive(), + /** Game minutes from a pilot becoming able to act to their next job (CR-020 §2). */ + turnaroundGameMinutes: z.number().positive(), + /** Empty board periods in a row away from home before heading home (0.5.124). */ + emptyPeriodsBeforeHome: positiveInt, + /** Real days of absence catch-up replays at most (0.5.125). */ + maxCatchUpRealDays: z.number().positive(), + /** Credits pilots never spend the company below, by default (0.5.123). */ + defaultSpendingFloor: positiveInt, + }), }) .refine( (config) => config.contractMassBands.reduce((sum, band) => sum + band.count, 0) === config.contractBoardSize, { message: "Mass band counts must add up to contractBoardSize", path: ["contractMassBands"] }, + ) + .refine( + (config) => + config.independentPilots.firstLookRealMinutes < config.contractBoardPeriodRealMinutes, + { + message: "The first-look window must be shorter than the board period", + path: ["independentPilots", "firstLookRealMinutes"], + }, ); export type GameConfig = z.infer; diff --git a/packages/content/test/config.test.ts b/packages/content/test/config.test.ts index bad8c79..0b9b293 100644 --- a/packages/content/test/config.test.ts +++ b/packages/content/test/config.test.ts @@ -96,3 +96,37 @@ describe("tutorial settings (Step 17.1, decisions 0.5.106, 0.5.107)", () => { ); }); }); + +describe("independent pilots config (Step 20.1, CR-020)", () => { + const settings = gameConfig.independentPilots; + const withSettings = (change: Partial) => + gameConfigSchema.safeParse({ ...gameConfig, independentPilots: { ...settings, ...change } }) + .success; + + it("has every setting, all positive, at the CR's defaults", () => { + const lighter = shipModels.find((s) => s.id === "lighter")!; + expect(settings).toEqual({ + dispatchOfficePrice: lighter.price, + firstLookRealMinutes: 3, + turnaroundGameMinutes: 15, + emptyPeriodsBeforeHome: 3, + maxCatchUpRealDays: 7, + defaultSpendingFloor: 500, + }); + for (const value of Object.values(settings)) expect(value).toBeGreaterThan(0); + }); + + it("rejects zero or negative values", () => { + for (const key of Object.keys(settings) as (keyof typeof settings)[]) { + expect(withSettings({ [key]: 0 }), key).toBe(false); + expect(withSettings({ [key]: -1 }), key).toBe(false); + } + }); + + it("keeps the first-look window shorter than the board period", () => { + expect(settings.firstLookRealMinutes).toBeLessThan(gameConfig.contractBoardPeriodRealMinutes); + expect(withSettings({ firstLookRealMinutes: gameConfig.contractBoardPeriodRealMinutes })).toBe( + false, + ); + }); +}); diff --git a/packages/content/test/schemas.test.ts b/packages/content/test/schemas.test.ts index 2dfc5c2..2ad197a 100644 --- a/packages/content/test/schemas.test.ts +++ b/packages/content/test/schemas.test.ts @@ -104,6 +104,14 @@ const validGameConfig = { skillPointsPerLevel: 1, pilotHireCost: 1000, pilotProfitShare: 0.25, + independentPilots: { + dispatchOfficePrice: 36_000, + firstLookRealMinutes: 3, + turnaroundGameMinutes: 15, + emptyPeriodsBeforeHome: 3, + maxCatchUpRealDays: 7, + defaultSpendingFloor: 500, + }, tutorialLoanerShipModelId: "company-courier", tutorialRunsBeforeDeal: 5, tutorialXpMultiplier: 2,