iv: add otium expenditure debits — three drift log entries per otium cycle
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
// OTIVM server — OTIVM-III complete
|
||||
// OTIVM server — OTIVM-IV
|
||||
// Per-player SQLite integration.
|
||||
//
|
||||
// Drift log trigger types:
|
||||
// newDen < prevDen → trigger_type = 'dispatch_cost' (cost deduction on dispatch)
|
||||
// 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)
|
||||
// aut band change → trigger_type = 'interval_complete' (otium rest)
|
||||
// otium event → trigger_type = 'otium_access_fee', 'personal_maintenance',
|
||||
// 'officia_obligation' (three separate entries)
|
||||
//
|
||||
// Otium expenditure constants — from docs/economy/cost-calibration-model.md:
|
||||
// OTIUM_ACCESS_FEE_DN = 2.00 (LOW confidence)
|
||||
// PERSONAL_MAINTENANCE_DN = 4.00 (MEDIUM confidence)
|
||||
// OFFICIA_OBLIGATION_DN = 2.00 (LOW confidence)
|
||||
// OTIUM_CYCLE_TOTAL_DN = 8.00
|
||||
//
|
||||
// Cost split — placeholder pending a proper cost model:
|
||||
// cost_vectura = 60% of route cost (VECTVRA — freight charge)
|
||||
@@ -31,6 +39,14 @@ const SCHEMA_PATH = join(ROOT, 'data', 'create_player_db.sql')
|
||||
|
||||
await mkdir(SAVES_DIR, { recursive: true })
|
||||
|
||||
// ── Otium expenditure constants ──────────────────────────────────────────────
|
||||
// Source: docs/economy/cost-calibration-model.md
|
||||
// Change only here — three separate drift log entries are written per otium cycle.
|
||||
const OTIUM_ACCESS_FEE_DN = 2.00 // LOW confidence
|
||||
const PERSONAL_MAINTENANCE_DN = 4.00 // MEDIUM confidence
|
||||
const OFFICIA_OBLIGATION_DN = 2.00 // LOW confidence
|
||||
const OTIUM_CYCLE_TOTAL_DN = OTIUM_ACCESS_FEE_DN + PERSONAL_MAINTENANCE_DN + OFFICIA_OBLIGATION_DN
|
||||
|
||||
// ── Cost split constants ─────────────────────────────────────────────────────
|
||||
const COST_VECTURA_RATIO = 0.60
|
||||
const COST_PORTORIA_RATIO = 0.25
|
||||
@@ -113,8 +129,6 @@ 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()
|
||||
@@ -233,10 +247,7 @@ function readPlayerState(token, pdb) {
|
||||
route_dispatches[ev.route_id] = (route_dispatches[ev.route_id] || 0) + 1
|
||||
}
|
||||
if (ev.type === 'journal_unlock' && ev.route_id) {
|
||||
journal_seen.push({
|
||||
routeId: ev.route_id,
|
||||
dispatch: route_dispatches[ev.route_id] || 1,
|
||||
})
|
||||
journal_seen.push({ routeId: ev.route_id, dispatch: route_dispatches[ev.route_id] || 1 })
|
||||
}
|
||||
if (ev.type === 'venture_start' || ev.type === 'dispatch_start') {
|
||||
try {
|
||||
@@ -508,6 +519,47 @@ function writePlayerState(token, pdb, body) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Otium event — debit liquiditas in three named components
|
||||
// Each component writes a separate drift log entry so the sub-trace
|
||||
// records the cause of each debit individually.
|
||||
if (ev.type === 'otium') {
|
||||
insertEvent.run(
|
||||
actorId, ev.type, null, null,
|
||||
JSON.stringify(ev), ts
|
||||
)
|
||||
|
||||
// Only debit if we have a prior liquiditas value to work from
|
||||
if (prevDen !== null) {
|
||||
let runningDen = parseFloat(prevDen)
|
||||
|
||||
const deductions = [
|
||||
{ amount: OTIUM_ACCESS_FEE_DN, trigger: 'otium_access_fee', note: 'Commercial information access and factor network maintenance.' },
|
||||
{ amount: PERSONAL_MAINTENANCE_DN, trigger: 'personal_maintenance', note: 'Food, lodging, clothing upkeep, light, and local movement.' },
|
||||
{ amount: OFFICIA_OBLIGATION_DN, trigger: 'officia_obligation', note: 'Patronage, tips, gifts, collegial contributions, and unrecovered favors.' },
|
||||
]
|
||||
|
||||
for (const deduction of deductions) {
|
||||
const denBefore = runningDen
|
||||
const denAfter = Math.max(0, runningDen - deduction.amount)
|
||||
runningDen = denAfter
|
||||
|
||||
writeDriftLog(
|
||||
pdb, actorId, 'liquiditas',
|
||||
deduction.trigger,
|
||||
null,
|
||||
String(denBefore), String(denAfter),
|
||||
deduction.note,
|
||||
ts
|
||||
)
|
||||
}
|
||||
|
||||
// Update liquiditas to the final post-otium value
|
||||
updateParam(pdb, actorId, 'liquiditas', String(runningDen), String(runningDen), ts)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
insertEvent.run(
|
||||
actorId, ev.type || 'unknown',
|
||||
routeId,
|
||||
@@ -524,36 +576,43 @@ function writePlayerState(token, pdb, body) {
|
||||
// aut band change → 'interval_complete' (otium rest)
|
||||
//
|
||||
// No drift log entry when prevDen/prevAut is null (initial seeding).
|
||||
// Liquiditas may already have been updated by the otium handler above —
|
||||
// the body.den update below handles dispatch and venture changes only.
|
||||
|
||||
if (body.den !== undefined) {
|
||||
const newDen = String(body.den)
|
||||
if (prevDen === null) {
|
||||
// Initial write — no drift log
|
||||
// Re-read current liquiditas in case otium handler already updated it
|
||||
const freshLiq = pdb.prepare(`
|
||||
SELECT value_true FROM actor_parameters
|
||||
WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL
|
||||
ORDER BY recorded_at DESC LIMIT 1
|
||||
`).get(actorId)
|
||||
const freshDen = freshLiq?.value_true ?? null
|
||||
|
||||
if (freshDen === null) {
|
||||
updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now)
|
||||
} else if (newDen !== prevDen) {
|
||||
} else if (newDen !== freshDen) {
|
||||
const numNew = parseFloat(newDen)
|
||||
const numPrev = parseFloat(prevDen)
|
||||
const numFresh = parseFloat(freshDen)
|
||||
updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now)
|
||||
|
||||
if (numNew < numPrev) {
|
||||
// Cost deduction — dispatch_start fired
|
||||
if (numNew < numFresh) {
|
||||
const active = findActiveVenture(pdb, actorId)
|
||||
writeDriftLog(
|
||||
pdb, actorId, 'liquiditas',
|
||||
'dispatch_cost',
|
||||
active?.venture_id || null,
|
||||
prevDen, newDen,
|
||||
freshDen, 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,
|
||||
freshDen, newDen,
|
||||
lastVenture?.venture_label || null,
|
||||
now
|
||||
)
|
||||
@@ -707,7 +766,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-III complete')
|
||||
console.log('OTIVM server running on port 3000 — OTIVM-IV')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user