iv: fix drift log trigger type — dispatch_cost vs venture_complete
This commit is contained in:
113
server/index.js
113
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)
|
||||
|
||||
Reference in New Issue
Block a user