Step 2.5: add the default game config
Rescales money values so tutorial runs pay meaningful amounts, and moves the game epoch from .env.example into GameConfig. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6804a7120c
commit
2c211524d3
|
|
@ -11,8 +11,3 @@ BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
# HTTP port the API server listens on
|
# HTTP port the API server listens on
|
||||||
PORT=3000
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
- **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.
|
- **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.
|
- **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).
|
||||||
|
|
|
||||||
|
|
@ -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.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.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.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
|
- [ ] Step 2.6 — Single content bundle
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,28 @@ export const cargoTypes: CargoType[] = [
|
||||||
{
|
{
|
||||||
id: "standard",
|
id: "standard",
|
||||||
name: "Standard",
|
name: "Standard",
|
||||||
basePayPerTonnePerMillionKm: 10,
|
basePayPerTonnePerMillionKm: 100,
|
||||||
tierPayMultiplier: 1,
|
tierPayMultiplier: 1,
|
||||||
requiredSkillId: null,
|
requiredSkillId: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "heavy",
|
id: "heavy",
|
||||||
name: "Heavy",
|
name: "Heavy",
|
||||||
basePayPerTonnePerMillionKm: 14,
|
basePayPerTonnePerMillionKm: 140,
|
||||||
tierPayMultiplier: 1.2,
|
tierPayMultiplier: 1.2,
|
||||||
requiredSkillId: "heavy-cargo",
|
requiredSkillId: "heavy-cargo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "high-value",
|
id: "high-value",
|
||||||
name: "High-Value",
|
name: "High-Value",
|
||||||
basePayPerTonnePerMillionKm: 18,
|
basePayPerTonnePerMillionKm: 180,
|
||||||
tierPayMultiplier: 1.25,
|
tierPayMultiplier: 1.25,
|
||||||
requiredSkillId: "expensive-cargo",
|
requiredSkillId: "expensive-cargo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "dangerous",
|
id: "dangerous",
|
||||||
name: "Dangerous",
|
name: "Dangerous",
|
||||||
basePayPerTonnePerMillionKm: 20,
|
basePayPerTonnePerMillionKm: 200,
|
||||||
tierPayMultiplier: 1.3,
|
tierPayMultiplier: 1.3,
|
||||||
requiredSkillId: "dangerous-cargo",
|
requiredSkillId: "dangerous-cargo",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -14,7 +14,7 @@ export const locations: Location[] = [
|
||||||
body: "earth",
|
body: "earth",
|
||||||
launchCostMultiplier: 3,
|
launchCostMultiplier: 3,
|
||||||
startsDiscovered: true,
|
startsDiscovered: true,
|
||||||
bayLeasePrice: 5000,
|
bayLeasePrice: 1000,
|
||||||
produces: ["standard", "high-value"],
|
produces: ["standard", "high-value"],
|
||||||
demands: ["heavy", "dangerous"],
|
demands: ["heavy", "dangerous"],
|
||||||
},
|
},
|
||||||
|
|
@ -26,7 +26,7 @@ export const locations: Location[] = [
|
||||||
body: "moon",
|
body: "moon",
|
||||||
launchCostMultiplier: 0.5,
|
launchCostMultiplier: 0.5,
|
||||||
startsDiscovered: true,
|
startsDiscovered: true,
|
||||||
bayLeasePrice: 3000,
|
bayLeasePrice: 800,
|
||||||
produces: ["standard", "heavy"],
|
produces: ["standard", "heavy"],
|
||||||
demands: ["standard", "high-value", "dangerous"],
|
demands: ["standard", "high-value", "dangerous"],
|
||||||
},
|
},
|
||||||
|
|
@ -38,7 +38,7 @@ export const locations: Location[] = [
|
||||||
body: "mars",
|
body: "mars",
|
||||||
launchCostMultiplier: 1.5,
|
launchCostMultiplier: 1.5,
|
||||||
startsDiscovered: false,
|
startsDiscovered: false,
|
||||||
bayLeasePrice: 8000,
|
bayLeasePrice: 20000,
|
||||||
produces: ["standard", "high-value"],
|
produces: ["standard", "high-value"],
|
||||||
demands: ["heavy", "dangerous"],
|
demands: ["heavy", "dangerous"],
|
||||||
},
|
},
|
||||||
|
|
@ -50,7 +50,7 @@ export const locations: Location[] = [
|
||||||
body: "ceres",
|
body: "ceres",
|
||||||
launchCostMultiplier: 0.3,
|
launchCostMultiplier: 0.3,
|
||||||
startsDiscovered: false,
|
startsDiscovered: false,
|
||||||
bayLeasePrice: 10000,
|
bayLeasePrice: 40000,
|
||||||
produces: ["heavy"],
|
produces: ["heavy"],
|
||||||
demands: ["standard", "high-value", "dangerous"],
|
demands: ["standard", "high-value", "dangerous"],
|
||||||
},
|
},
|
||||||
|
|
@ -62,7 +62,7 @@ export const locations: Location[] = [
|
||||||
body: "jupiter",
|
body: "jupiter",
|
||||||
launchCostMultiplier: 2,
|
launchCostMultiplier: 2,
|
||||||
startsDiscovered: false,
|
startsDiscovered: false,
|
||||||
bayLeasePrice: 15000,
|
bayLeasePrice: 80000,
|
||||||
produces: ["dangerous"],
|
produces: ["dangerous"],
|
||||||
demands: ["standard", "high-value", "heavy"],
|
demands: ["standard", "high-value", "heavy"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const shipModels: ShipModel[] = [
|
||||||
id: "courier",
|
id: "courier",
|
||||||
name: "Courier",
|
name: "Courier",
|
||||||
archetype: "courier",
|
archetype: "courier",
|
||||||
price: 30000,
|
price: 4000,
|
||||||
cruiseSpeedKmPerGameHour: 3_000_000,
|
cruiseSpeedKmPerGameHour: 3_000_000,
|
||||||
cargoCapacityTonnes: 20,
|
cargoCapacityTonnes: 20,
|
||||||
// Moon only.
|
// Moon only.
|
||||||
|
|
@ -18,13 +18,13 @@ export const shipModels: ShipModel[] = [
|
||||||
hullRating: 1,
|
hullRating: 1,
|
||||||
landingClass: "atmospheric",
|
landingClass: "atmospheric",
|
||||||
overheadGameHours: 2,
|
overheadGameHours: 2,
|
||||||
fuelCostPerMillionKm: 20,
|
fuelCostPerMillionKm: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "freighter",
|
id: "freighter",
|
||||||
name: "Freighter",
|
name: "Freighter",
|
||||||
archetype: "freighter",
|
archetype: "freighter",
|
||||||
price: 150000,
|
price: 100000,
|
||||||
cruiseSpeedKmPerGameHour: 2_000_000,
|
cruiseSpeedKmPerGameHour: 2_000_000,
|
||||||
cargoCapacityTonnes: 100,
|
cargoCapacityTonnes: 100,
|
||||||
// Mars and Ceres at any distance; Jupiter only near its closest (decision 0.5.25).
|
// 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,
|
hullRating: 2,
|
||||||
landingClass: "atmospheric",
|
landingClass: "atmospheric",
|
||||||
overheadGameHours: 4,
|
overheadGameHours: 4,
|
||||||
fuelCostPerMillionKm: 40,
|
fuelCostPerMillionKm: 400,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bulk-hauler",
|
id: "bulk-hauler",
|
||||||
name: "Bulk Hauler",
|
name: "Bulk Hauler",
|
||||||
archetype: "bulk-hauler",
|
archetype: "bulk-hauler",
|
||||||
price: 600000,
|
price: 400000,
|
||||||
cruiseSpeedKmPerGameHour: 1_200_000,
|
cruiseSpeedKmPerGameHour: 1_200_000,
|
||||||
cargoCapacityTonnes: 400,
|
cargoCapacityTonnes: 400,
|
||||||
// Jupiter at its farthest.
|
// Jupiter at its farthest.
|
||||||
|
|
@ -46,6 +46,6 @@ export const shipModels: ShipModel[] = [
|
||||||
hullRating: 3,
|
hullRating: 3,
|
||||||
landingClass: "atmospheric",
|
landingClass: "atmospheric",
|
||||||
overheadGameHours: 8,
|
overheadGameHours: 8,
|
||||||
fuelCostPerMillionKm: 80,
|
fuelCostPerMillionKm: 800,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ export * from "./schemas";
|
||||||
export * from "./data/locations";
|
export * from "./data/locations";
|
||||||
export * from "./data/ships";
|
export * from "./data/ships";
|
||||||
export * from "./data/cargo";
|
export * from "./data/cargo";
|
||||||
|
export * from "./data/config";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue