diff --git a/.env.example b/.env.example index 544379d..164e7d9 100644 --- a/.env.example +++ b/.env.example @@ -11,8 +11,3 @@ BETTER_AUTH_URL=http://localhost:3000 # HTTP port the API server listens on PORT=3000 - -# Game clock epoch (decision 0.5.9): the real instant the game clock starts, -# and the in-game ephemeris date it maps to. Both ISO 8601, UTC. -GAME_EPOCH_REAL_START=2026-01-01T00:00:00Z -GAME_EPOCH_EPHEMERIS_DATE=2100-01-01T00:00:00Z diff --git a/DECISIONS.md b/DECISIONS.md index 427a9d4..5d12c1f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -63,3 +63,12 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **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. + +## Step 2.5 + +- **The game epoch lives in GameConfig, not in env vars.** Step 2.1 puts it in GameConfig, and rule 0.1.5 keeps game numbers in `packages/content`. The `GAME_EPOCH_*` variables added to `.env.example` in Step 1.5 are removed; the placeholder values (real start 2026-01-01, in-game 2100-01-01) are unchanged. +- **Money values scaled up.** With the Step 2.2–2.4 rates a tutorial run paid ~24 credits. Pay rates and fuel costs are ×10, and prices are set so a Courier (4,000) plus an Earth bay (1,000) takes ~21 runs at ~240 credits each. Freighter 100,000, Bulk Hauler 400,000; leases Moon 800, Mars 20,000, Ceres 40,000, Jupiter 80,000. +- **Placeholder tutorial payout in the test:** Standard pay × 0.384M km × average mass (midpoint of the contract minimum and Courier capacity, 12.5 t) × tutorial share (0.5). +- **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). diff --git a/memory-bank/progress.md b/memory-bank/progress.md index a9df07a..414ec54 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -14,5 +14,5 @@ - [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. - [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 +- [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 diff --git a/packages/content/src/data/cargo.ts b/packages/content/src/data/cargo.ts index 7142a77..b782339 100644 --- a/packages/content/src/data/cargo.ts +++ b/packages/content/src/data/cargo.ts @@ -5,28 +5,28 @@ export const cargoTypes: CargoType[] = [ { id: "standard", name: "Standard", - basePayPerTonnePerMillionKm: 10, + basePayPerTonnePerMillionKm: 100, tierPayMultiplier: 1, requiredSkillId: null, }, { id: "heavy", name: "Heavy", - basePayPerTonnePerMillionKm: 14, + basePayPerTonnePerMillionKm: 140, tierPayMultiplier: 1.2, requiredSkillId: "heavy-cargo", }, { id: "high-value", name: "High-Value", - basePayPerTonnePerMillionKm: 18, + basePayPerTonnePerMillionKm: 180, tierPayMultiplier: 1.25, requiredSkillId: "expensive-cargo", }, { id: "dangerous", name: "Dangerous", - basePayPerTonnePerMillionKm: 20, + basePayPerTonnePerMillionKm: 200, tierPayMultiplier: 1.3, requiredSkillId: "dangerous-cargo", }, diff --git a/packages/content/src/data/config.ts b/packages/content/src/data/config.ts new file mode 100644 index 0000000..77844b0 --- /dev/null +++ b/packages/content/src/data/config.ts @@ -0,0 +1,29 @@ +import type { GameConfig } from "../schemas"; + +/** Default tuning. All numbers are first guesses until balancing in Phase 12. */ +export const gameConfig: GameConfig = { + // 1 real minute = 1 game hour. + timeCompression: 60, + // Placeholders: the design docs do not fix an in-game start date yet. + epoch: { + realStart: "2026-01-01T00:00:00Z", + ephemerisDate: "2100-01-01T00:00:00Z", + }, + startingCredits: 0, + tutorialPayoutShare: 0.5, + contractBoardSize: 8, + // A tutorial run takes about 2 real minutes, so one hour-long period allows ~30. + tutorialBoardSize: 40, + contractBoardPeriodRealMinutes: 60, + firstContactChance: 0.1, + firstContactPayoutMultiplier: 2, + contractMassTonnes: { min: 5, max: 400 }, + // Mars at its closest needs no Long Haul; Jupiter at its farthest needs level 5. + longHaulCutoffsMillionKm: [100, 250, 400, 600, 800], + xpPerDelivery: { base: 10, perMillionKm: 0.2, perCargoTier: 5 }, + pilotLevelXpThresholds: [100, 250, 450, 700, 1000, 1400, 1900, 2500, 3200, 4000], + // Ten levels × 2 points = enough to max all four skills (4 × 5 levels). + skillPointsPerLevel: 2, + pilotHireCost: 2000, + pilotPayPerDelivery: 100, +}; diff --git a/packages/content/src/data/locations.ts b/packages/content/src/data/locations.ts index 275d04e..0c040e4 100644 --- a/packages/content/src/data/locations.ts +++ b/packages/content/src/data/locations.ts @@ -14,7 +14,7 @@ export const locations: Location[] = [ body: "earth", launchCostMultiplier: 3, startsDiscovered: true, - bayLeasePrice: 5000, + bayLeasePrice: 1000, produces: ["standard", "high-value"], demands: ["heavy", "dangerous"], }, @@ -26,7 +26,7 @@ export const locations: Location[] = [ body: "moon", launchCostMultiplier: 0.5, startsDiscovered: true, - bayLeasePrice: 3000, + bayLeasePrice: 800, produces: ["standard", "heavy"], demands: ["standard", "high-value", "dangerous"], }, @@ -38,7 +38,7 @@ export const locations: Location[] = [ body: "mars", launchCostMultiplier: 1.5, startsDiscovered: false, - bayLeasePrice: 8000, + bayLeasePrice: 20000, produces: ["standard", "high-value"], demands: ["heavy", "dangerous"], }, @@ -50,7 +50,7 @@ export const locations: Location[] = [ body: "ceres", launchCostMultiplier: 0.3, startsDiscovered: false, - bayLeasePrice: 10000, + bayLeasePrice: 40000, produces: ["heavy"], demands: ["standard", "high-value", "dangerous"], }, @@ -62,7 +62,7 @@ export const locations: Location[] = [ body: "jupiter", launchCostMultiplier: 2, startsDiscovered: false, - bayLeasePrice: 15000, + bayLeasePrice: 80000, produces: ["dangerous"], demands: ["standard", "high-value", "heavy"], }, diff --git a/packages/content/src/data/ships.ts b/packages/content/src/data/ships.ts index 75a50b4..5fb0e53 100644 --- a/packages/content/src/data/ships.ts +++ b/packages/content/src/data/ships.ts @@ -10,7 +10,7 @@ export const shipModels: ShipModel[] = [ id: "courier", name: "Courier", archetype: "courier", - price: 30000, + price: 4000, cruiseSpeedKmPerGameHour: 3_000_000, cargoCapacityTonnes: 20, // Moon only. @@ -18,13 +18,13 @@ export const shipModels: ShipModel[] = [ hullRating: 1, landingClass: "atmospheric", overheadGameHours: 2, - fuelCostPerMillionKm: 20, + fuelCostPerMillionKm: 200, }, { id: "freighter", name: "Freighter", archetype: "freighter", - price: 150000, + price: 100000, cruiseSpeedKmPerGameHour: 2_000_000, cargoCapacityTonnes: 100, // Mars and Ceres at any distance; Jupiter only near its closest (decision 0.5.25). @@ -32,13 +32,13 @@ export const shipModels: ShipModel[] = [ hullRating: 2, landingClass: "atmospheric", overheadGameHours: 4, - fuelCostPerMillionKm: 40, + fuelCostPerMillionKm: 400, }, { id: "bulk-hauler", name: "Bulk Hauler", archetype: "bulk-hauler", - price: 600000, + price: 400000, cruiseSpeedKmPerGameHour: 1_200_000, cargoCapacityTonnes: 400, // Jupiter at its farthest. @@ -46,6 +46,6 @@ export const shipModels: ShipModel[] = [ hullRating: 3, landingClass: "atmospheric", overheadGameHours: 8, - fuelCostPerMillionKm: 80, + fuelCostPerMillionKm: 800, }, ]; diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 3d0b1c1..4f0f98b 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -2,3 +2,4 @@ export * from "./schemas"; export * from "./data/locations"; export * from "./data/ships"; export * from "./data/cargo"; +export * from "./data/config"; diff --git a/packages/content/test/config.test.ts b/packages/content/test/config.test.ts new file mode 100644 index 0000000..93942dd --- /dev/null +++ b/packages/content/test/config.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { + cargoTypes, + gameConfig, + gameConfigSchema, + locations, + shipModels, + skills, +} from "../src/index"; + +// Placeholder average tutorial payout until Phase 12 uses real sim numbers: +// Standard cargo over ~0.384M km (Earth–Moon), at the average mass a Courier +// can carry from the configured range, times the tutorial share. +const EARTH_MOON_MILLION_KM = 0.384; + +describe("game config", () => { + it("passes the schema", () => { + expect(() => gameConfigSchema.parse(gameConfig)).not.toThrow(); + }); + + it("uses 1 real minute = 1 game hour", () => { + expect(gameConfig.timeCompression).toBe(60); + }); + + it("has one Long Haul cut-off per Long Haul level", () => { + const longHaul = skills.find((s) => s.id === "long-haul")!; + expect(gameConfig.longHaulCutoffsMillionKm).toHaveLength(longHaul.maxLevel); + }); + + it("makes a Courier plus an Earth bay affordable after 15–25 tutorial runs", () => { + const courier = shipModels.find((s) => s.id === "courier")!; + const earth = locations.find((l) => l.id === "earth")!; + const standard = cargoTypes.find((c) => c.id === "standard")!; + + const averageMass = (gameConfig.contractMassTonnes.min + courier.cargoCapacityTonnes) / 2; + const averageTutorialPayout = + standard.basePayPerTonnePerMillionKm * + averageMass * + EARTH_MOON_MILLION_KM * + gameConfig.tutorialPayoutShare; + + const runs = (courier.price + earth.bayLeasePrice) / averageTutorialPayout; + expect(runs).toBeGreaterThanOrEqual(15); + expect(runs).toBeLessThanOrEqual(25); + }); +});