diff --git a/server/index.js b/server/index.js index 9a5d5ff..2543ef5 100644 --- a/server/index.js +++ b/server/index.js @@ -1,4 +1,4 @@ -// OTIVM server — OTIVM-III +// OTIVM server — OTIVM-IV // Per-player SQLite integration. // TESSERA world database (data/otivm.sqlite3) — read-only, unchanged. // Player databases (data/saves/{token}.sqlite3) — one per player, @@ -7,6 +7,12 @@ // JSON save files (data/saves/{token}.json) — never deleted, migrated // transparently on first SQLite access if present. // Frontend interface unchanged — GET/POST /api/save/:token same as before. +// +// Change 3 (OTIVM-IV): background_id from the frontend is now a real +// canonical identifier (e.g. 'former_legionary'). On new player creation, +// seed actor_parameters from background_starting_values for that background. +// The previous default of 'unknown' is kept only as a fallback for legacy +// saves that predate Change 3. import Fastify from 'fastify' import fastifyStatic from '@fastify/static' @@ -17,10 +23,10 @@ import { existsSync } from 'fs' import Database from 'better-sqlite3' const __dirname = dirname(fileURLToPath(import.meta.url)) -const ROOT = join(__dirname, '..') -const DIST = join(ROOT, 'dist') -const SAVES_DIR = join(ROOT, 'data', 'saves') -const DB_PATH = join(ROOT, 'data', 'otivm.sqlite3') +const ROOT = join(__dirname, '..') +const DIST = join(ROOT, 'dist') +const SAVES_DIR = join(ROOT, 'data', 'saves') +const DB_PATH = join(ROOT, 'data', 'otivm.sqlite3') const SCHEMA_PATH = join(ROOT, 'data', 'create_player_db.sql') await mkdir(SAVES_DIR, { recursive: true }) @@ -36,10 +42,10 @@ const stmtEpoch = db.prepare( const stmtH7 = db.prepare(` SELECT h7, - AVG(lat) AS lat, - AVG(lon) AS lon, - SUM(CASE WHEN elev_cm > ? THEN 1 ELSE 0 END) AS h7_land, - COUNT(*) AS h9_total + AVG(lat) AS lat, + AVG(lon) AS lon, + SUM(CASE WHEN elev_cm > ? THEN 1 ELSE 0 END) AS h7_land, + COUNT(*) AS h9_total FROM tessera_cells WHERE h5 = ? AND status = 2 GROUP BY h7 @@ -296,6 +302,13 @@ function autToInt(band) { // Writes parameter changes as new actor_parameters rows (append-only). // Appends new events to the events table. // Marks superseded parameter rows. +// +// On new player creation (no actor_profile row yet): +// - Uses background_id from the request body (sent by frontend after +// player chooses on the Prologue tab). +// - Seeds actor_parameters from background_starting_values for that background. +// - Sets liquiditas to the background's starting_den value from the body. +// - Falls back to 'unknown' only if no background_id is present (legacy saves). function writePlayerState(token, pdb, body) { const now = new Date().toISOString() @@ -307,29 +320,57 @@ function writePlayerState(token, pdb, body) { ).get(actorId) if (!profile) { + // New player — use the background_id sent by the frontend. + // The Prologue tab ensures this is a canonical identifier before sending. + const backgroundId = body.background_id || 'unknown' + const actorName = body.actor_name || 'Mercator' + pdb.prepare(` INSERT OR IGNORE INTO actor_profile (actor_id, session_id, background_id, actor_name, epoch, schema_version, recorded_at) - VALUES (?, ?, ?, ?, 'roman_14bce', 3, ?) - `).run( - actorId, - actorId, - body.background_id || 'unknown', - body.actor_name || 'Mercator', - now - ) - // Seed parameters from background_starting_values + VALUES (?, ?, ?, ?, 'roman_14bce', 4, ?) + `).run(actorId, actorId, backgroundId, actorName, now) + + // Seed all parameters from background_starting_values for chosen background. + // auctoritas gets value_social seeded from the same row's value_true. pdb.prepare(` INSERT OR IGNORE INTO actor_parameters (actor_id, parameter_token, scope, layer, - value_true, value_perceived, confidence_tag, observable_level, - drift_source, recorded_at) - SELECT ?, parameter_token, 'actor', 'roman', - value_true, value_perceived, confidence_tag, observable_level, - 'initial', ? + value_true, value_perceived, value_social, + confidence_tag, observable_level, drift_source, recorded_at) + SELECT + ?, parameter_token, 'actor', + CASE WHEN parameter_token IN ('auctoritas','clientela','liquiditas','fama', + 'disciplina','mercatus_scientia','itineris_scientia','ius_accessus', + 'periculum_tolerantia','negotiatio','litterae','officia_burden') + THEN 'roman' ELSE 'universal' END, + value_true, value_perceived, + CASE WHEN parameter_token = 'auctoritas' THEN value_true ELSE NULL END, + confidence_tag, observable_level, 'initial', ? FROM background_starting_values WHERE background_id = ? - `).run(actorId, now, body.background_id || 'unknown') + `).run(actorId, now, backgroundId) + + // Override liquiditas with the den value from the body (which was set + // from BACKGROUNDS[x].starting_den on the frontend). This is the + // authoritative starting value — the seed row is superseded immediately. + if (body.den !== undefined) { + // Supersede the seed row + pdb.prepare(` + UPDATE actor_parameters + SET superseded_at = ? + WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL + `).run(now, actorId) + // Insert the confirmed starting value + pdb.prepare(` + INSERT INTO actor_parameters + (actor_id, parameter_token, scope, layer, + value_true, value_perceived, + confidence_tag, observable_level, drift_source, recorded_at) + VALUES (?, 'liquiditas', 'actor', 'roman', + ?, ?, 'measured', 'full', 'initial', ?) + `).run(actorId, String(body.den), String(body.den), now) + } } // Update liquiditas if den changed @@ -540,7 +581,7 @@ fastify.setNotFoundHandler((req, reply) => { try { await fastify.listen({ port: 3000, host: '0.0.0.0' }) - console.log('OTIVM server running on port 3000 — OTIVM-III') + console.log('OTIVM server running on port 3000 — OTIVM-IV') } catch (err) { console.error(err) process.exit(1)