deadhead-space-freight-tycoon/DECISIONS.md

12 KiB
Raw Blame History

Decisions

Implementation decisions not spelled out in memory-bank/implementation-plan.md. Design decisions from plan §0.5 are logged here as they are implemented.

Step 1.1

  • Repo lives on an NTFS drive (C:\Projects\...). The original location (G:) is exFAT, which cannot hold symlinks, so pnpm could not link workspace packages. Do not move the repo back to an exFAT drive.
  • TypeScript is pinned to ~6.0. TypeScript 7 is the latest release, but typescript-eslint (needed in Step 1.2) only supports <6.1. Upgrade once typescript-eslint supports 7.
  • Packages export TypeScript source directly (main/types point at src/index.ts). There is no build step for internal packages; Vite, Vitest and the server's TS runner consume the source. The base config uses moduleResolution: "Bundler" and noEmit.
  • Node 24 is the required runtime (engines + .nvmrc).
  • Line endings are LF everywhere (.gitattributes), overriding the machine's global core.autocrlf, to match Prettier and the Linux CI runner.

Step 1.2

  • ESLint 10 flat config with typescript-eslint recommended (not type-aware, to keep lint fast) and eslint-config-prettier. pnpm lint runs ESLint then prettier --check.
  • Purity rule for packages/sim and packages/content: no-restricted-properties bans Math.random and Date.now; no-restricted-syntax bans new Date() with no arguments (new Date(x) is allowed).
  • Prettier skips memory-bank/ so tooling never rewrites the design docs.

Step 1.3

  • Vitest projects: the root vitest.config.ts treats each apps/* and packages/* folder as a project, so pnpm test runs every package's tests. Packages can add their own vitest.config.ts when they need a different environment.
  • Tests live in each package's test/ folder and are included in that package's tsconfig, so pnpm typecheck covers them.

Step 1.4

  • CI runs on Gitea Actions (.gitea/workflows/ci.yml): install with a frozen lockfile, then typecheck, lint and test.
  • .pnpm-store/ is ignored by Prettier, ESLint and git. On the Gitea runner, pnpm puts its store inside the workspace, and prettier --check failed on the store's files (CI run #1).

Step 1.5

  • One Postgres container, two databases. docker-compose.yml runs a single postgres:18 server on port 5432 (user/password deadhead). The default database deadhead is for development; docker/postgres/init/01-create-test-db.sql creates deadhead_test the first time the data volume is created. To recreate both from scratch, run docker compose down -v.
  • Env var names in .env.example: DATABASE_URL, TEST_DATABASE_URL, BETTER_AUTH_SECRET and BETTER_AUTH_URL (Better Auth's own names), PORT, and GAME_EPOCH_REAL_START plus GAME_EPOCH_EPHEMERIS_DATE (decision 0.5.9). The game-clock speed is not an env var; it belongs in GameConfig (rule 0.1.5).
  • Epoch values in .env.example are placeholders. The design docs do not fix an ephemeris date yet.

Step 2.1

  • Ids are lowercase kebab-case strings (earth, bulk-hauler, long-haul).
  • One landing-class enum (vacuum, atmospheric, hardened) serves both a location's landing requirement and a ship's landing class. hardened is not used in the base game.
  • Location body is an enum of the five astronomy bodies (earth, moon, mars, ceres, jupiter) so the sim can map each to a position source in Step 3.3.
  • Epoch fields are ISO 8601 UTC strings (epoch.realStart, epoch.ephemerisDate), not Date objects, so content stays plain data.
  • tutorialBoardSize is a GameConfig field in addition to contractBoardSize, because decision 0.5.21 sets the tutorial board's size separately.
  • XP per delivery inputs are base, perMillionKm and perCargoTier. Step 3.16 decides how they combine.
  • pilotLevelXpThresholds lists the total XP needed for level 1, 2, 3, ...; it and the Long Haul cut-offs must be strictly increasing.
  • tierPayMultiplier is one number per cargo type. Step 3.8 decides how it scales with tier.
  • Imports omit the .ts extension (moduleResolution: "Bundler").

Step 2.2

  • Location ids: earth, moon, mars, ceres-station, jupiter-cloud-platform.
  • Cargo flows follow GDD §9: Earth and Mars produce standard and high-value goods and demand ore (heavy) and fuel (dangerous); the Moon produces standard and heavy; Ceres produces heavy; Jupiter produces dangerous (fuel). The outposts import high-value equipment.
  • Launch multipliers: Earth 3, Jupiter Cloud Platform 2, Mars 1.5, Moon 0.5, Ceres 0.3.
  • Bay lease prices are first guesses (Earth 5,000 up to Jupiter 15,000). Step 2.5 tunes the Earth price against the tutorial.
  • The cargo-id check uses a placeholder list of the four cargo ids planned in Step 2.4 until the cargo data exists.

Step 2.3

  • Ranges (million km): Courier 5 (Moon only; Mars is ~55 at its closest), Freighter 650 (covers Ceres at its farthest, ~600, and Jupiter near its closest, ~590, per decision 0.5.25), Bulk Hauler 1,000 (Jupiter at its farthest, ~970). Step 3.7 checks these against real positions.
  • Speeds and overheads aim at GDD §6 durations at compression 60: Courier 3M km/h with 2 h overhead (Earth→Moon ≈ 2 real minutes, mostly overhead), Freighter 2M km/h with 4 h (Earth→Mars ≈ 1–2 real hours), Bulk Hauler 1.2M km/h with 8 h.
  • Prices (30,000 / 150,000 / 600,000), capacities (20 / 100 / 400 t), fuel costs (20 / 40 / 80 per million km) and hull ratings (1 / 2 / 3) are first guesses; Step 2.5 tunes the Courier price.

Step 2.4

  • 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).

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.

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.

Step 3.2

  • Seeds are 32-bit unsigned integers. The PRNG is mulberry32; child seeds are an FNV-1a hash of the label, started from the parent seed, plus a final mixing step.
  • createRng returns an object with internal state. It is still deterministic: the same seed always gives the same sequence. Each use (a board, a name) creates its own generator from a derived seed, so draws never depend on call order elsewhere.

Step 3.3

  • Positions are heliocentric, in the J2000 mean ecliptic plane (HelioVector rotated by Rotation_EQJ_ECL), keeping x and y and dropping the small out-of-plane z.
  • The Moon is Earth plus 0.384M km on the +x axis, as the plan specifies; its real orbit is ignored.
  • Ceres uses a circular orbit (astronomy-engine has no Ceres): radius 2.7675 AU (~414M km), period 1,680.5 days, mean longitude ~160° at J2000. The angle is approximate, which is fine for gameplay.
  • Test date is 2100-03-01, close to the placeholder game epoch.

Step 3.4

  • Route distance is the straight line between the two positions at one game time. Orbital transfer paths are not modelled.

Step 3.5

  • Travel time takes the distance as a number, not two locations and a time. The caller measures the distance at departure, so the function cannot accidentally use a mid-flight distance.

Step 3.7

  • Landing is checked before range, so a ship that can neither land nor reach reports cannot-land.
  • The route check looks only at the destination's landing requirement. A ship is always already sitting at its origin, so it can take off from there.
  • Range tests find each planet's closest and farthest point from Earth by sampling every 4 days over 12 years. All Step 2.3 range choices hold, including decision 0.5.25 (a Freighter reaches Jupiter at its closest).

Step 3.8

  • Tier scaling is compounding: pay is multiplied by tierPayMultiplier ** tier. Standard (tier 0) is ×1; a tier-5 Dangerous contract pays 1.3⁵ ≈ 3.7× its base rate.
  • Payouts are rounded to whole credits when the contract is offered.
  • Contracts refer to content by id (originId, destinationId, cargoTypeId), so they can be stored and sent over the API as plain data. requiredSkill is { skillId, level }, or null for Standard.

Step 3.9

  • Board seed: deriveSeed(worldSeed, "board:<bayLocationId>:<periodIndex>"), with one generator per board.
  • Board periods count from the Unix epoch: period index = floor(real ms ÷ period length). Contracts are offered at the start of their period, and their payout and Long Haul level use the distance at that moment, so a board generated later in the period is identical (decision 0.5.5).
  • Contract ids are <origin>:<periodIndex>:<slot>, unique within a player's world. The server may add its own database ids.
  • First contact is rolled only when an undiscovered location exists. If no other location is discovered, every contract is first contact. Destinations are picked uniformly from the eligible list.
  • Mass is a whole number of tonnes drawn uniformly from the configured range. Tier is drawn uniformly from 1 to the required skill's max level.
  • Sim functions take a SimContent object (locations, cargoTypes, skills, config); the content bundle satisfies it, and tests can pass fixtures.