Step 3.1: add game clock conversion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-02 18:40:19 -04:00
parent 241e2613f4
commit f44789878e
7 changed files with 84 additions and 2 deletions

View File

@ -77,3 +77,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **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.
## Step 3.1
- **Times are Unix-millisecond numbers.** A real time is a wall-clock instant; a game time is the instant of the in-game date, so `new Date(gameTime)` gives the in-game date and can be passed straight to `astronomy-engine`.
- **Every sim function takes the content it needs as arguments** (here the `GameConfig`), rather than importing the default bundle, so tests can pass custom fixtures.

View File

@ -16,3 +16,24 @@
- [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.
- [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.
## Phase 3 — Simulation Package
- [x] **Step 3.1 — Game clock conversion.** `src/clock.ts`: `realToGame` / `gameToReal` using the config epoch and compression; times are Unix-ms numbers.
- [ ] Step 3.2 — Seeded random number generator
- [ ] Step 3.3 — Location positions
- [ ] Step 3.4 — Route distance
- [ ] Step 3.5 — Travel time
- [ ] Step 3.6 — Fuel cost
- [ ] Step 3.7 — Range and landing eligibility
- [ ] Step 3.8 — Contract model and payout
- [ ] Step 3.9 — Contract board generation
- [ ] Step 3.10 — Tutorial contract board
- [ ] Step 3.11 — Pilot eligibility
- [ ] Step 3.12 — Dispatch validation
- [ ] Step 3.13 — Dispatch record creation
- [ ] Step 3.14 — Reposition dispatch
- [ ] Step 3.15 — In-flight position and progress
- [ ] Step 3.16 — XP and leveling
- [ ] Step 3.17 — Dispatch resolution
- [ ] Step 3.18 — Sim package public API and coverage

View File

@ -7,5 +7,8 @@
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@deadhead/content": "workspace:*"
}
}

26
packages/sim/src/clock.ts Normal file
View File

@ -0,0 +1,26 @@
import type { GameConfig } from "@deadhead/content";
/**
* Times are milliseconds since the Unix epoch. A real time is a wall-clock
* instant; a game time is the instant of the in-game (ephemeris) date, so
* `new Date(gameTime)` is the in-game date.
*/
export type RealTime = number;
export type GameTime = number;
export const MS_PER_MINUTE = 60_000;
export const MS_PER_HOUR = 3_600_000;
/** Converts a real instant to the in-game instant it maps to. */
export function realToGame(real: RealTime, config: GameConfig): GameTime {
const realStart = Date.parse(config.epoch.realStart);
const gameStart = Date.parse(config.epoch.ephemerisDate);
return gameStart + (real - realStart) * config.timeCompression;
}
/** Converts an in-game instant back to the real instant it maps to. */
export function gameToReal(game: GameTime, config: GameConfig): RealTime {
const realStart = Date.parse(config.epoch.realStart);
const gameStart = Date.parse(config.epoch.ephemerisDate);
return realStart + (game - gameStart) / config.timeCompression;
}

View File

@ -1 +1 @@
export {};
export * from "./clock";

View File

@ -0,0 +1,23 @@
import { gameConfig } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import { MS_PER_HOUR, MS_PER_MINUTE, gameToReal, realToGame } from "../src/index";
const realStart = Date.parse(gameConfig.epoch.realStart);
const gameStart = Date.parse(gameConfig.epoch.ephemerisDate);
describe("game clock", () => {
it("maps the real epoch to the game epoch", () => {
expect(realToGame(realStart, gameConfig)).toBe(gameStart);
});
it("round-trips real → game → real", () => {
for (const real of [realStart, realStart + 12_345_678, Date.parse("2027-06-15T08:30:00Z")]) {
expect(gameToReal(realToGame(real, gameConfig), gameConfig)).toBe(real);
}
});
it("advances one game hour per real minute at compression 60", () => {
expect(gameConfig.timeCompression).toBe(60);
expect(realToGame(realStart + MS_PER_MINUTE, gameConfig)).toBe(gameStart + MS_PER_HOUR);
});
});

View File

@ -48,7 +48,11 @@ importers:
packages/schema: {}
packages/sim: {}
packages/sim:
dependencies:
'@deadhead/content':
specifier: workspace:*
version: link:../content
packages: