diff --git a/src/screens/Forum.jsx b/src/screens/Forum.jsx index 5b60115..5efc4cf 100644 --- a/src/screens/Forum.jsx +++ b/src/screens/Forum.jsx @@ -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.den100?'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 && (
{message}