iv: fix route selection and otium den deduction in Forum.jsx

This commit is contained in:
otivm
2026-05-03 18:54:40 +00:00
parent e13b54a697
commit 7aed095f69

View File

@@ -2,6 +2,10 @@
// FORUM context screen. Replaces Ledger.jsx.
// Renders two divs directly into Shell's two-col layout grid.
// All game logic migrated from Ledger.jsx.
//
// Fixes from initial version:
// - isRouteUnlocked called with route.id (string), not route (object)
// - applyOtium result has 8 dn deducted to match server-side debit
import { useState, useEffect, useRef } from 'react'
import Section from '../components/Section.jsx'
@@ -27,7 +31,6 @@ export default function Forum({ state, onStateChange, onNewGame }) {
const [otium, setOtium] = useState(null)
const [message, setMessage] = useState('')
const [journal, setJournal] = useState(getSeenJournalEntries(state))
const [newEntryKey, setNewEntryKey] = useState(null)
const tickRef = useRef(null)
const msgRef = useRef(null)
@@ -48,11 +51,7 @@ export default function Forum({ state, onStateChange, onNewGame }) {
const newState = applyDispatch(state, dispatch.routeId)
const route = ROUTES.find(r => r.id === dispatch.routeId)
showMessage(`Galley returned. +${route.profit} denarii.`)
if (entry) {
setJournal(j => [entry, ...j])
setNewEntryKey(`${dispatch.routeId}-${newState.route_dispatches[dispatch.routeId]}`)
setTimeout(() => setNewEntryKey(null), 5000)
}
if (entry) setJournal(j => [entry, ...j])
setSelectedRoute(null)
onStateChange(newState)
}
@@ -61,8 +60,10 @@ export default function Forum({ state, onStateChange, onNewGame }) {
const elapsed = now - otium.startMs
if (elapsed >= OTIUM_DURATION_MS) {
setOtium(null)
const newState = applyOtium(state)
showMessage('Otium complete. Auctoritas recorded.')
// Apply otium aut gain, then deduct 8 dn to match server-side debit
const withAut = applyOtium(state)
const newState = { ...withAut, den: Math.max(0, withAut.den - OTIUM_CYCLE_TOTAL_DN) }
showMessage(`Otium complete. ${OTIUM_CYCLE_TOTAL_DN} dn. Auctoritas recorded.`)
onStateChange(newState)
}
}
@@ -74,6 +75,7 @@ export default function Forum({ state, onStateChange, onNewGame }) {
function handleSelectRoute(routeId) {
if (busy) return
// Fix: pass route.id string to isRouteUnlocked, not the route object
if (!isRouteUnlocked(state, routeId)) return
setSelectedRoute(prev => prev === routeId ? null : routeId)
}
@@ -103,85 +105,84 @@ export default function Forum({ state, onStateChange, onNewGame }) {
}
// Progress
let progressPct = 0, progressLabel = '', progressSub = ''
let progressPct = 0
if (dispatch) {
progressPct = Math.min(((Date.now()-dispatch.startMs)/dispatch.durationMs)*100,100)
const route = ROUTES.find(r=>r.id===dispatch.routeId)
progressLabel = `${WAYPOINTS[route.from].name}${WAYPOINTS[route.to].name}`
progressSub = `${Math.round(progressPct)}%`
progressPct = Math.min(((Date.now()-dispatch.startMs)/dispatch.durationMs)*100, 100)
} else if (otium) {
progressPct = Math.min(((Date.now()-otium.startMs)/OTIUM_DURATION_MS)*100,100)
progressLabel = 'resting...'
progressSub = `${Math.round(progressPct)}%`
progressPct = Math.min(((Date.now()-otium.startMs)/OTIUM_DURATION_MS)*100, 100)
}
// Section data
// Section data — active venture
const activeVentureItems = dispatch ? (() => {
const route = ROUTES.find(r=>r.id===dispatch.routeId)
const route = ROUTES.find(r => r.id === dispatch.routeId)
return [
{key:'Route', value:`${WAYPOINTS[route.from].name}${WAYPOINTS[route.to].name}`, cls:''},
{key:'Cargo', value:route.goods, cls:''},
{key:'Progress', value:`${Math.round(progressPct)}%`, cls:'good'},
{key:'Net', value:`+${route.profit-route.cost} dn expected`, cls:''},
{ key: 'Route', value: `${WAYPOINTS[route.from].name}${WAYPOINTS[route.to].name}`, cls: '' },
{ key: 'Cargo', value: route.goods, cls: '' },
{ key: 'Progress', value: `${Math.round(progressPct)}%`, cls: 'good' },
{ key: 'Net', value: `+${route.profit - route.cost} dn expected`, cls: '' },
]
})() : otium ? [
{key:'Status', value:'Otium in progress', cls:'warn'},
{key:'Progress', value:`${Math.round(progressPct)}%`, cls:''},
{key:'Cost', value:`${OTIUM_CYCLE_TOTAL_DN} dn`, cls:'bad'},
] : [{key:'Status', value:'No galley at sea', cls:''}]
{ key: 'Status', value: 'Otium in progress', cls: 'warn' },
{ key: 'Progress', value: `${Math.round(progressPct)}%`, cls: '' },
{ key: 'Cost', value: `${OTIUM_CYCLE_TOTAL_DN} dn`, cls: 'bad' },
] : [{ key: 'Status', value: 'No galley at sea', cls: '' }]
// Route list — fix: pass route.id to isRouteUnlocked
const routeItems = ROUTES.map(route => {
const unlocked = isRouteUnlocked(state, route)
const vectura = Math.round(route.cost*0.60*10)/10
const portoria = Math.round(route.cost*0.25*10)/10
const other = Math.round((route.cost-vectura-portoria)*10)/10
const unlocked = isRouteUnlocked(state, route.id)
const vectura = Math.round(route.cost * 0.60 * 10) / 10
const portoria = Math.round(route.cost * 0.25 * 10) / 10
const other = Math.round((route.cost - vectura - portoria) * 10) / 10
return {
id: route.id,
name: `${WAYPOINTS[route.from].name}${WAYPOINTS[route.to].name}`,
goods: route.goods,
mode: (route.id==='grain'||route.id==='linen')?'sea':'road',
days: Math.round(route.duration_ms/3000),
mode: (route.id === 'grain' || route.id === 'linen') ? 'sea' : 'road',
days: Math.round(route.duration_ms / 3000),
cost: route.cost,
profit: route.profit,
vectura, portoria, other,
locked: !unlocked,
lock_reason: !unlocked ? (state.den<route.unlock_den ? `${route.unlock_den.toLocaleString()} dn` : `Auctoritas ${route.unlock_aut}`) : null,
lock_reason: !unlocked
? (state.den < route.unlock_den
? `${route.unlock_den.toLocaleString()} dn`
: `Auctoritas ${route.unlock_aut}`)
: null,
}
})
const chapter = getChapter(state.den, state.aut)
const standingItems = [
{key:'Denarii', value:`${Math.floor(state.den)} dn`, cls:state.den>100?'good':'warn'},
{key:'Auctoritas', value:`${Math.floor(state.aut)}`, cls:''},
{key:'Chapter', value:`${['I','II','III','IV','V'][chapter-1]} · ${['Ostia','Capua','Brundisium','Carthago','Alexandria'][chapter-1]}`, cls:''},
{key:'Dispatches', value:`${state.dispatches||0} complete`, cls:''},
{ key: 'Denarii', value: `${Math.floor(state.den)} dn`, cls: state.den > 100 ? 'good' : 'warn' },
{ key: 'Auctoritas', value: `${Math.floor(state.aut)}`, cls: '' },
{ key: 'Chapter', value: `${['I','II','III','IV','V'][chapter-1]} · ${['Ostia','Capua','Brundisium','Carthago','Alexandria'][chapter-1]}`, cls: '' },
{ key: 'Dispatches', value: `${state.dispatches || 0} complete`, cls: '' },
]
const expenditureItems = [
{label:'OTIVM access', amount:`${OTIUM_ACCESS_FEE_DN} dn`, period:'per otium cycle', source:'cost-calibration-model.md', conf:'LOW', debit:true},
{label:'Personal maintenance', amount:`${PERSONAL_MAINTENANCE_DN} dn`, period:'per otium cycle', source:'cost-calibration-model.md', conf:'MEDIUM', debit:true},
{label:'Officia obligations', amount:`${OFFICIA_OBLIGATION_DN} dn`, period:'per otium cycle', source:'cost-calibration-model.md', conf:'LOW', debit:true},
{ label: 'OTIVM access', amount: `${OTIUM_ACCESS_FEE_DN} dn`, period: 'per otium cycle', source: 'cost-calibration-model.md', conf: 'LOW', debit: true },
{ label: 'Personal maintenance', amount: `${PERSONAL_MAINTENANCE_DN} dn`, period: 'per otium cycle', source: 'cost-calibration-model.md', conf: 'MEDIUM', debit: true },
{ label: 'Officia obligations', amount: `${OFFICIA_OBLIGATION_DN} dn`, period: 'per otium cycle', source: 'cost-calibration-model.md', conf: 'LOW', debit: true },
]
const journalEntries = (journal.length>0 ? journal : [{
day:'Day 1 · Ostia',
text:'The harbour smells of pitch and ambition. I have fifty denarii and a single battered ledger. The factor tells me the olive route to Capua is open.',
}]).map(e=>({date:e.day, text:e.text}))
const journalEntries = (journal.length > 0 ? journal : [{
day: 'Day 1 · Ostia',
text: 'The harbour smells of pitch and ambition. I have fifty denarii and a single battered ledger. The factor tells me the olive route to Capua is open.',
}]).map(e => ({ date: e.day, text: e.text }))
const actionButtons = [
{label:'Dispatch galley', style:'otivm-btn-dispatch', action:'dispatch', disabled:busy||!selectedRoute},
{label:'Take otium', style:'otivm-btn-otium', action:'otium', disabled:busy},
{ label: 'Dispatch galley', style: 'otivm-btn-dispatch', action: 'dispatch', disabled: busy || !selectedRoute },
{ label: 'Take otium', style: 'otivm-btn-otium', action: 'otium', disabled: busy },
]
return (
<>
{message && (
<div style={{
position:'fixed',bottom:'20px',right:'20px',
background:'var(--otivm-ink)',color:'var(--otivm-parch)',
padding:'10px 18px',borderRadius:'3px',
fontSize:'0.8rem',fontStyle:'italic',zIndex:300,
gridColumn:'1/-1',
position: 'fixed', bottom: '20px', right: '20px',
background: 'var(--otivm-ink)', color: 'var(--otivm-parch)',
padding: '10px 18px', borderRadius: '3px',
fontSize: '0.8rem', fontStyle: 'italic', zIndex: 300,
}}>
{message}
</div>