111 lines
6.1 KiB
TypeScript
111 lines
6.1 KiB
TypeScript
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||
|
||
// Drizzle schema for the game state. Column names/types here are the source of
|
||
// truth; the matching CREATE TABLE statements live in lib/db/client.ts (DDL) and
|
||
// must stay in sync with this file.
|
||
|
||
export const planes = sqliteTable('planes', {
|
||
id: text('id').primaryKey().notNull(),
|
||
model: text('model').notNull(),
|
||
// v0.39.0 (ISS-069): 'repairing' = grounded in the shop until repairUntilSimTime passes; the
|
||
// engine flips it back to 'available' on a tick. All status guards key off 'available'.
|
||
status: text('status', { enum: ['available', 'flying', 'repairing'] }).notNull(),
|
||
fuel: real('fuel').notNull(),
|
||
maxFuel: real('max_fuel').notNull(),
|
||
locationIcao: text('location_icao').notNull(),
|
||
// Current flight destination (v0.12.0) — set by fly, cleared on arrival. The
|
||
// plane's origin is always its locationIcao while flying.
|
||
destIcao: text('dest_icao'),
|
||
lat: real('lat').notNull(),
|
||
lon: real('lon').notNull(),
|
||
// Sim-clock departure time of the current flight (null when available). While
|
||
// flying, `fuel` is frozen at its departure value — live position/fuel are pure
|
||
// functions of the clock (lib/sim/movement.ts), recomputed on read.
|
||
departureSimTime: integer('departure_sim_time'),
|
||
// Queue of upcoming legs NOT yet started (v0.25.0, ISS-043) — ordered JSON array
|
||
// of ICAO strings ('[]' when empty). The ACTIVE leg stays destIcao +
|
||
// departureSimTime; on landing the engine auto-chains queue[0] if fuel covers it.
|
||
queue: text('queue').notNull().default('[]'),
|
||
// Structural health in [0, 100] (v0.39.0, ISS-069) — wears down by WEAR_PER_KM per km of
|
||
// landed legs; at 0 the plane is broken and cannot fly until repaired (v0.45.0, ISS-089).
|
||
health: real('health').notNull().default(100),
|
||
// Sim-clock epoch ms when an active repair finishes (v0.39.0, ISS-069) — set with status
|
||
// 'repairing', cleared by the engine's flip-back pass; NULL = not in repair.
|
||
repairUntilSimTime: integer('repair_until_sim_time'),
|
||
});
|
||
|
||
// Cargo packages (v0.12.0) replace the old contracts table end-to-end: a package
|
||
// is one cargo unit origin → dest with a reward and deadline. 'loaded' = bound to
|
||
// a plane's slot; it stays bound ('expired') or pays out ('delivered') when the
|
||
// plane arrives at its destination airport.
|
||
export const packages = sqliteTable('packages', {
|
||
id: text('id').primaryKey().notNull(),
|
||
originIcao: text('origin_icao').notNull(),
|
||
destIcao: text('dest_icao').notNull(),
|
||
reward: real('reward').notNull(),
|
||
cargoType: text('cargo_type').notNull(),
|
||
cargoCount: integer('cargo_count').notNull(),
|
||
// Package weight in kg (v0.27.0, ISS-045) — drives load validation (plane payload)
|
||
// and the per-kg part of the payout. Pre-existing rows backfill to the default.
|
||
weightKg: real('weight_kg').notNull().default(2000),
|
||
// Special status (v0.37.0, ISS-073) — key into lib/game/specials.ts SPECIALS; NULL = none.
|
||
special: text('special'),
|
||
deadlineSimTime: integer('deadline_sim_time').notNull(),
|
||
// 'expired' = an open OR loaded package whose deadline passed on a tick. Loaded
|
||
// expired packages keep occupying their slot, deliver no reward, and abandon free.
|
||
status: text('status', { enum: ['open', 'loaded', 'delivered', 'expired', 'abandoned'] }).notNull(),
|
||
planeId: text('plane_id'),
|
||
createdSimTime: integer('created_sim_time').notNull(),
|
||
// Real-time (Date.now()) epoch ms of creation (v0.65.0) — open jobs live on the WALL clock, not the sim
|
||
// clock: they expire JOB_LIFETIME_MS after this regardless of time scale or fleet state. Legacy rows
|
||
// backfill to seed time so they get a fresh full lifetime instead of expiring on the first tick.
|
||
createdRealMs: integer('created_real_ms').notNull().default(0),
|
||
// Real-time epoch ms when this (origin, band) slot was last taken — loaded (v0.65.0). The pool
|
||
// spawner holds that slot for the band's refill delay after; NULL = never taken / no cooldown. Expired open
|
||
// jobs set nothing: an untaken job's slot respawns immediately.
|
||
slotFreeAtRealMs: integer('slot_free_at_real_ms'),
|
||
});
|
||
|
||
export const account = sqliteTable('account', {
|
||
cash: real('cash').notNull(),
|
||
reputation: real('reputation').notNull(),
|
||
});
|
||
|
||
export const simState = sqliteTable('sim_state', {
|
||
clockMs: integer('clock_ms').notNull(),
|
||
timeScale: real('time_scale').notNull(),
|
||
paused: integer('paused', { mode: 'boolean' }).notNull(),
|
||
// Real-time (Date.now()) of the last tick — the tick-on-read engine advances
|
||
// clockMs by (now - lastRealTickMs) × timeScale, clamped per tick.
|
||
lastRealTickMs: integer('last_real_tick_ms').notNull(),
|
||
// Last MANUALLY selected speed (v0.30.0, ISS-057) — landings slow the sim to 1× without
|
||
// touching this; flyPlane restores it when the last grounded plane takes off. Written by
|
||
// POST /api/sim whenever timeScale is posted; pre-existing rows backfill to time_scale.
|
||
lastSelectedTimeScale: real('last_selected_time_scale').notNull().default(1),
|
||
});
|
||
|
||
// v0.71.0: accounts + login sessions for the admin panel (and later multiplayer auth).
|
||
// The seeded admin row is inserted by lib/db/client.ts on every open() — hash-only, see there.
|
||
export const users = sqliteTable('users', {
|
||
id: text('id').primaryKey().notNull(),
|
||
username: text('username').notNull().unique(),
|
||
// scrypt digest as "salt$hex" (16-byte hex salt, 32-byte hex hash) — verified with node:crypto.
|
||
passwordHash: text('password_hash').notNull(),
|
||
admin: integer('admin', { mode: 'boolean' }).notNull().default(false),
|
||
createdAt: integer('created_at').notNull(),
|
||
});
|
||
|
||
export const sessions = sqliteTable('sessions', {
|
||
id: text('id').primaryKey().notNull(),
|
||
userId: text('user_id').notNull(),
|
||
// Real-time (Date.now()) epoch ms — 7 days from creation; expired rows are pruned lazily on login.
|
||
expiresAt: integer('expires_at').notNull(),
|
||
});
|
||
|
||
export type PlaneRow = typeof planes.$inferSelect;
|
||
export type PackageRow = typeof packages.$inferSelect;
|
||
export type AccountRow = typeof account.$inferSelect;
|
||
export type SimStateRow = typeof simState.$inferSelect;
|
||
export type UserRow = typeof users.$inferSelect;
|
||
export type SessionRow = typeof sessions.$inferSelect;
|