diff --git a/server/index.js b/server/index.js index 43d9e2b..a5d42f4 100644 --- a/server/index.js +++ b/server/index.js @@ -1,20 +1,17 @@ -// OTIVM server — OTIVM-IV +// OTIVM server — OTIVM-III complete // Per-player SQLite integration. // -// Fix 1: if actor_profile exists but background_id = 'unknown' and the -// incoming body has a real background_id, patch the profile and seed -// actor_parameters from background_starting_values. This handles the -// race where the first POST (background selection save) arrives before -// the background_id is set in the body. -// -// Fix 2: drift log entries are suppressed when there is no prior -// parameter row (prevDen = null / first write). This eliminates noise -// rows from the initial seeding sequence. +// Drift log trigger types: +// newDen < prevDen → trigger_type = 'dispatch_cost' (cost deduction on dispatch) +// newDen > prevDen → trigger_type = 'venture_complete' (return profit) +// newAut band change → trigger_type = 'interval_complete' (otium rest) // // Cost split — placeholder pending a proper cost model: // cost_vectura = 60% of route cost (VECTVRA — freight charge) // cost_portoria = 25% of route cost (PORTORIUM — customs duty) // cost_other = 15% of route cost (horreum, incidentals) +// +// JSON migration: retained per roadmap — JSON files are never deleted. import Fastify from 'fastify' import fastifyStatic from '@fastify/static' @@ -116,9 +113,11 @@ function uuid() { } // ── JSON → SQLite migration ────────────────────────────────────────────────── +// Retained per roadmap — JSON files are never deleted. +// Migration runs transparently on first SQLite access if a .json exists. function migrateJsonToSqlite(token, pdb, json) { - const now = new Date().toISOString() + const now = new Date().toISOString() const actorId = token const sessionId = token const background = json.background_id || 'unknown' @@ -293,9 +292,7 @@ function autToInt(band) { } } -// ── Seed actor_parameters from background_starting_values ──────────────────── -// Extracted as a standalone function so it can be called both at new-player -// creation and at the background-patch step (Fix 1). +// ── Seed parameters ────────────────────────────────────────────────────────── function seedParameters(pdb, actorId, backgroundId, denOverride, now) { pdb.prepare(` @@ -316,12 +313,9 @@ function seedParameters(pdb, actorId, backgroundId, denOverride, now) { WHERE background_id = ? `).run(actorId, now, backgroundId) - // Override liquiditas with the confirmed starting den from the frontend. - // Supersede the seed row immediately so the true starting value is authoritative. if (denOverride !== undefined) { pdb.prepare(` - UPDATE actor_parameters - SET superseded_at = ? + UPDATE actor_parameters SET superseded_at = ? WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL `).run(now, actorId) pdb.prepare(` @@ -344,6 +338,14 @@ function findActiveVenture(pdb, actorId) { `).get(actorId) } +function findLastCompletedVenture(pdb, actorId) { + return pdb.prepare(` + SELECT venture_id, venture_label FROM ventures + WHERE actor_id = ? AND status = 'complete' + ORDER BY completed_at DESC LIMIT 1 + `).get(actorId) +} + function createVenture(pdb, actorId, routeId, now) { const eco = ROUTE_ECONOMICS[routeId] const cargo = ROUTE_CARGO[routeId] @@ -392,8 +394,7 @@ function closeVenture(pdb, actorId, routeId, ventureId, now) { WHERE venture_id = ? `).run(eco.profit, outcomeNet, now, ventureId) pdb.prepare(` - UPDATE venture_legs - SET status = 'complete', completed_at = ? + UPDATE venture_legs SET status = 'complete', completed_at = ? WHERE venture_id = ? AND status = 'active' `).run(now, ventureId) } @@ -420,15 +421,11 @@ function writePlayerState(token, pdb, body) { const incomingBackground = body.background_id const isRealBackground = incomingBackground && incomingBackground !== 'unknown' - // Load existing profile (may or may not exist) const profile = pdb.prepare( 'SELECT actor_id, background_id FROM actor_profile WHERE actor_id = ? LIMIT 1' ).get(actorId) if (!profile) { - // Brand new player — create profile regardless of background_id. - // If background is still 'unknown', parameters are not seeded yet; - // they will be seeded on the next POST that arrives with a real background. const backgroundId = incomingBackground || 'unknown' const actorName = body.actor_name || 'Mercator' @@ -443,18 +440,15 @@ function writePlayerState(token, pdb, body) { } } else if (profile.background_id === 'unknown' && isRealBackground) { - // Fix 1: profile exists but background was not set on first write. - // Patch the profile and seed parameters now. pdb.prepare(` UPDATE actor_profile SET background_id = ?, schema_version = 5 WHERE actor_id = ? `).run(incomingBackground, actorId) seedParameters(pdb, actorId, incomingBackground, body.den, now) - console.log(`[profile] Patched background_id for ${token}: unknown → ${incomingBackground}`) } - // Read current parameter values — needed for drift log (Fix 2: null check) + // Read current values before any update — null if no prior row exists const currentLiq = pdb.prepare(` SELECT value_true FROM actor_parameters WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL @@ -466,7 +460,6 @@ function writePlayerState(token, pdb, body) { ORDER BY recorded_at DESC LIMIT 1 `).get(actorId) - // Fix 2: only record drift when a prior row exists (not during initial seeding) const prevDen = currentLiq?.value_true ?? null const prevAut = currentAut?.value_true ?? null @@ -524,35 +517,55 @@ function writePlayerState(token, pdb, body) { } // ── Update parameters + drift log ──────────────────────────────────────── + // + // Drift log trigger types: + // newDen < prevDen → 'dispatch_cost' (cost deducted at dispatch) + // newDen > prevDen → 'venture_complete' (profit returned on completion) + // aut band change → 'interval_complete' (otium rest) + // + // No drift log entry when prevDen/prevAut is null (initial seeding). if (body.den !== undefined) { const newDen = String(body.den) - if (prevDen !== null && newDen !== prevDen) { - const lastVenture = pdb.prepare(` - SELECT venture_id, venture_label FROM ventures - WHERE actor_id = ? AND status = 'complete' - ORDER BY completed_at DESC LIMIT 1 - `).get(actorId) - + if (prevDen === null) { + // Initial write — no drift log + updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now) + } else if (newDen !== prevDen) { + const numNew = parseFloat(newDen) + const numPrev = parseFloat(prevDen) updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now) - writeDriftLog( - pdb, actorId, 'liquiditas', - 'venture_complete', - lastVenture?.venture_id || null, - prevDen, newDen, - lastVenture?.venture_label || null, - now - ) - } else if (prevDen === null) { - // No prior row — update silently, no drift log entry - updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now) + if (numNew < numPrev) { + // Cost deduction — dispatch_start fired + const active = findActiveVenture(pdb, actorId) + writeDriftLog( + pdb, actorId, 'liquiditas', + 'dispatch_cost', + active?.venture_id || null, + prevDen, newDen, + active?.venture_label || null, + now + ) + } else { + // Profit return — dispatch_complete fired + const lastVenture = findLastCompletedVenture(pdb, actorId) + writeDriftLog( + pdb, actorId, 'liquiditas', + 'venture_complete', + lastVenture?.venture_id || null, + prevDen, newDen, + lastVenture?.venture_label || null, + now + ) + } } } if (body.aut !== undefined) { const newBand = autBand(body.aut) - if (prevAut !== null && newBand !== prevAut) { + if (prevAut === null) { + updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now) + } else if (newBand !== prevAut) { updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now) writeDriftLog( pdb, actorId, 'auctoritas', @@ -562,8 +575,6 @@ function writePlayerState(token, pdb, body) { 'otium rest', now ) - } else if (prevAut === null) { - updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now) } } } @@ -696,7 +707,7 @@ fastify.setNotFoundHandler((req, reply) => { reply.sendFile('index.html') }) try { await fastify.listen({ port: 3000, host: '0.0.0.0' }) - console.log('OTIVM server running on port 3000 — OTIVM-IV (fix: background_id patch)') + console.log('OTIVM server running on port 3000 — OTIVM-III complete') } catch (err) { console.error(err) process.exit(1)