4.4 KiB
4.4 KiB
Architecture
Living reference for how the code is organised. Update it after any database schema or API change (implementation plan rule 0.1.9). Decisions and their reasons live in DECISIONS.md.
Packages
| Package | Role | Depends on |
|---|---|---|
packages/content |
Static game data (locations, ship models, cargo, skills, GameConfig) as Zod-validated TypeScript, plus the content bundle with id lookups. |
zod |
packages/sim |
Pure, deterministic game rules: clock, seeded RNG, positions, distances, travel, fuel, eligibility, contract boards, dispatch validation/records, flight progress, XP, resolution. No I/O, no Date.now(), no Math.random(). |
content, astronomy-engine |
packages/schema |
Zod schemas for every API request, response and error. Shared by server and client. | sim (error codes), zod |
apps/server |
Fastify API, Postgres via Drizzle, pg-boss workers. The authority on outcomes. | content, sim, schema |
apps/web |
React client. Predicts with sim, never decides outcomes. | content, sim, schema |
Conventions
- Times are Unix milliseconds (numbers). A real time is a wall-clock instant; a game time is the instant of the in-game date (
realToGame/gameToRealin sim). - Ids are non-empty strings. Content ids are kebab-case (
earth,bulk-hauler). - Credits are whole numbers.
- Static vs. player data: the API never sends static data. The client imports
@deadhead/contentand joins by id.
API data model (packages/schema)
| Shape | Fields |
|---|---|
ServerTime |
realTime, gameTime |
Company |
id, name, credits |
Ship |
id, modelId, name, status (idle / in-flight), locationId (null in flight), dispatchId (null when idle), pilotId |
Pilot |
id, name, isAvatar, xp, level, skills ({ skillId: level }), skillPointsAvailable, assignedShipId, status, locationId (null in flight) |
Bay |
id, locationId |
LocationState |
id, discovered |
Contract |
mirrors sim Contract |
Dispatch |
mirrors sim DispatchRecord + status (in-flight / resolved), resolvedAt |
ResolutionSummary |
dispatchId, shipId, pilotId, originId, destinationId, tutorial, credits, pilotXp, discoveredLocationId, resolvedAt |
TutorialState |
loanerLocationId (earth / moon), runInFlight |
PlayerState |
company, ships, pilots, bays, locations, tutorial (null once over), activeDispatches, summaries |
Errors
Every non-2xx response has the body { error: { code, message } }. code is one of ERROR_CODES: every sim dispatch and reposition validation reason (DISPATCH_FAILURES, REPOSITION_FAILURES), unchanged, plus the server codes in SERVER_ERROR_CODES (validation-error, unauthorized, not-found, insufficient-funds, ...).
API routes (packages/schema/src/routes.ts)
The routes catalog is the single source for both server and client. Each entry has method, path, request (path parameters merged with the body), response (2xx body) and auth. Errors always use the error body above.
| Name | Method & path | Request | Response | Auth |
|---|---|---|---|---|
getServerTime |
GET /api/time |
— | ServerTime |
no |
createGuest |
POST /api/guest |
— | { playerId, state: PlayerState } + session cookie |
no |
getPlayerState |
GET /api/state |
— | PlayerState |
yes |
getContractBoard |
GET /api/bays/:locationId/board |
locationId |
{ contracts, periodEndsAt } |
yes |
getTutorialBoard |
GET /api/tutorial/board |
— | { contracts, periodEndsAt } |
yes |
createDispatch |
POST /api/dispatches |
{ kind: "contract", contractId, shipId, pilotId } / { kind: "tutorial", contractId } / { kind: "reposition", shipId, pilotId, destinationId } |
{ dispatch } |
yes |
buyShip |
POST /api/ships |
modelId |
{ ship } |
yes |
leaseBay |
POST /api/bays |
locationId |
{ bay } |
yes |
hirePilot |
POST /api/pilots |
locationId (a player bay) |
{ pilot } |
yes |
assignPilot |
POST /api/pilots/:pilotId/assign |
pilotId, shipId |
{ pilot } |
yes |
unassignPilot |
POST /api/pilots/:pilotId/unassign |
pilotId |
{ pilot } |
yes |
spendSkillPoint |
POST /api/pilots/:pilotId/skills |
pilotId, skillId |
{ pilot } |
yes |
acknowledgeSummaries |
POST /api/summaries/acknowledge |
dispatchIds |
{ acknowledged } |
yes |