import { integer, primaryKey, real, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' // Drizzle schema for game state. Single-player by design: exactly ONE save row ever exists // (no auth/accounts), so every table hangs off saves.id but only one save is in play. // Column names/types here are the source of truth; matching CREATE TABLE statements live // in lib/db/client.ts (DDL) and must stay in sync with this file. export const saves = sqliteTable('saves', { id: integer('id').primaryKey({ autoIncrement: true }), // Seed for the deterministic world generator — same seed always regenerates the same world. seed: text('seed').notNull(), turn: integer('turn').notNull().default(0), createdAt: text('created_at').notNull(), // Which schema version this save was last brought up to (NULL on saves from before the // column existed = v1). ensureSave runs ordered idempotent backfill steps for anything below. schemaVersion: integer('schema_version'), }) export const solarSystems = sqliteTable('solar_systems', { id: integer('id').primaryKey({ autoIncrement: true }), saveId: integer('save_id').notNull().references(() => saves.id), name: text('name').notNull(), // 0–100 coordinate space, rendered directly by the NodeGraph component. x: real('x').notNull(), y: real('y').notNull(), discovered: integer('discovered', { mode: 'boolean' }).notNull().default(false), isCurrent: integer('is_current', { mode: 'boolean' }).notNull().default(false), }) // Jump routes, stored as directed pairs (both directions of each route) so adjacency and // "reachable from current" are single-row lookups. Read side dedupes for display. export const jumpRoutes = sqliteTable( 'jump_routes', { fromSystem: integer('from_system').notNull().references(() => solarSystems.id), toSystem: integer('to_system').notNull().references(() => solarSystems.id), }, (t) => [primaryKey({ columns: [t.fromSystem, t.toSystem] })], ) // Bodies of a solar system: planets (parent_id NULL) and moons (parent_id = their planet's // id). Generated when the system is generated, so every system has 2–5 planets, some with // 1–3 moons. Insertion order is display order — a moon always follows its parent row. export const planets = sqliteTable('planets', { id: integer('id').primaryKey({ autoIncrement: true }), systemId: integer('system_id').notNull().references(() => solarSystems.id), parentId: integer('parent_id'), // NULL for a planet, set for a moon name: text('name').notNull(), type: text('type').notNull(), hazard: integer('hazard', { mode: 'boolean' }).notNull().default(false), explored: integer('explored', { mode: 'boolean' }).notNull().default(false), }) // Dumb-simple cargo hold on the save: what exploration has found so far. SHIP SYSTEMS (#8) // will spend parts from here later; nothing consumes it yet. export const inventoryItems = sqliteTable('inventory_items', { id: integer('id').primaryKey({ autoIncrement: true }), saveId: integer('save_id').notNull().references(() => saves.id), kind: text('kind').notNull(), // 'resource' | 'part' name: text('name').notNull(), qty: integer('qty').notNull().default(1), }) // The ship's crew roster: the starting crew (seeded on new game) plus members found while // exploring planets/moons. Display-only for now — no dismiss/assign actions yet (#7 scope). export const crew = sqliteTable('crew', { id: integer('id').primaryKey({ autoIncrement: true }), saveId: integer('save_id').notNull().references(() => saves.id), name: text('name').notNull(), role: text('role').notNull(), traitGood: text('trait_good').notNull(), traitBad: text('trait_bad').notNull(), // Where the member came from: 'STARTING CREW' or ', ' for found members. origin: text('origin').notNull(), }) // The ship's systems, one row per known type (see SHIP_TYPES in lib/game/ship-costs.ts). // `built` is derived, not stored: level > 0 means built — unbuilt rows sit at level/capacity 0. export const shipSystems = sqliteTable( 'ship_systems', { id: integer('id').primaryKey({ autoIncrement: true }), saveId: integer('save_id').notNull().references(() => saves.id), type: text('type').notNull(), level: integer('level').notNull().default(0), // 0 = not built yet capacity: integer('capacity').notNull().default(0), }, (t) => [uniqueIndex('ship_systems_save_type_unique').on(t.saveId, t.type)], ) // The technology tree, one row per known tech (see TECHS in lib/game/tech.ts). Definitions are // seeded from the pure module on new game / backfill v5 — same pattern as ship_systems. // `researched` is the per-save progress; prerequisites/effect are stored for completeness but // the canonical semantics live in lib/game/tech.ts (EFFECTS). export const tech = sqliteTable( 'tech', { id: integer('id').primaryKey({ autoIncrement: true }), saveId: integer('save_id').notNull().references(() => saves.id), slug: text('slug').notNull(), // stable key, e.g. WEAPONS_SYSTEMS name: text('name').notNull(), // display name, e.g. 'WEAPONS SYSTEMS' costKind: text('cost_kind').notNull(), // 'resource' | 'part' costQty: integer('cost_qty').notNull().default(1), prerequisites: text('prerequisites').notNull().default(''), // comma-separated tech slugs, '' when none effect: text('effect').notNull(), // effect slug — semantics in EFFECTS (lib/game/tech.ts) researched: integer('researched', { mode: 'boolean' }).notNull().default(false), }, (t) => [uniqueIndex('tech_save_slug_unique').on(t.saveId, t.slug)], ) // Append-only game log. Writers so far: new-game + jump events; later issues add more // writers and a read endpoint. export const events = sqliteTable('events', { id: integer('id').primaryKey({ autoIncrement: true }), saveId: integer('save_id').notNull().references(() => saves.id), turn: integer('turn').notNull(), type: text('type').notNull(), message: text('message').notNull(), createdAt: text('created_at').notNull(), })