iv: fix route selection and otium den deduction in Forum.jsx
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
// FORUM context screen. Replaces Ledger.jsx.
|
// FORUM context screen. Replaces Ledger.jsx.
|
||||||
// Renders two divs directly into Shell's two-col layout grid.
|
// Renders two divs directly into Shell's two-col layout grid.
|
||||||
// All game logic migrated from Ledger.jsx.
|
// 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 { useState, useEffect, useRef } from 'react'
|
||||||
import Section from '../components/Section.jsx'
|
import Section from '../components/Section.jsx'
|
||||||
@@ -27,7 +31,6 @@ export default function Forum({ state, onStateChange, onNewGame }) {
|
|||||||
const [otium, setOtium] = useState(null)
|
const [otium, setOtium] = useState(null)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [journal, setJournal] = useState(getSeenJournalEntries(state))
|
const [journal, setJournal] = useState(getSeenJournalEntries(state))
|
||||||
const [newEntryKey, setNewEntryKey] = useState(null)
|
|
||||||
const tickRef = useRef(null)
|
const tickRef = useRef(null)
|
||||||
const msgRef = useRef(null)
|
const msgRef = useRef(null)
|
||||||
|
|
||||||
@@ -48,11 +51,7 @@ export default function Forum({ state, onStateChange, onNewGame }) {
|
|||||||
const newState = applyDispatch(state, dispatch.routeId)
|
const newState = applyDispatch(state, dispatch.routeId)
|
||||||
const route = ROUTES.find(r => r.id === dispatch.routeId)
|
const route = ROUTES.find(r => r.id === dispatch.routeId)
|
||||||
showMessage(`Galley returned. +${route.profit} denarii.`)
|
showMessage(`Galley returned. +${route.profit} denarii.`)
|
||||||
if (entry) {
|
if (entry) setJournal(j => [entry, ...j])
|
||||||
setJournal(j => [entry, ...j])
|
|
||||||
setNewEntryKey(`${dispatch.routeId}-${newState.route_dispatches[dispatch.routeId]}`)
|
|
||||||
setTimeout(() => setNewEntryKey(null), 5000)
|
|
||||||
}
|
|
||||||
setSelectedRoute(null)
|
setSelectedRoute(null)
|
||||||
onStateChange(newState)
|
onStateChange(newState)
|
||||||
}
|
}
|
||||||
@@ -61,8 +60,10 @@ export default function Forum({ state, onStateChange, onNewGame }) {
|
|||||||
const elapsed = now - otium.startMs
|
const elapsed = now - otium.startMs
|
||||||
if (elapsed >= OTIUM_DURATION_MS) {
|
if (elapsed >= OTIUM_DURATION_MS) {
|
||||||
setOtium(null)
|
setOtium(null)
|
||||||
const newState = applyOtium(state)
|
// Apply otium aut gain, then deduct 8 dn to match server-side debit
|
||||||
showMessage('Otium complete. Auctoritas recorded.')
|
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)
|
onStateChange(newState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +75,7 @@ export default function Forum({ state, onStateChange, onNewGame }) {
|
|||||||
|
|
||||||
function handleSelectRoute(routeId) {
|
function handleSelectRoute(routeId) {
|
||||||
if (busy) return
|
if (busy) return
|
||||||
|
// Fix: pass route.id string to isRouteUnlocked, not the route object
|
||||||
if (!isRouteUnlocked(state, routeId)) return
|
if (!isRouteUnlocked(state, routeId)) return
|
||||||
setSelectedRoute(prev => prev === routeId ? null : routeId)
|
setSelectedRoute(prev => prev === routeId ? null : routeId)
|
||||||
}
|
}
|
||||||
@@ -103,85 +105,84 @@ export default function Forum({ state, onStateChange, onNewGame }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Progress
|
// Progress
|
||||||
let progressPct = 0, progressLabel = '', progressSub = ''
|
let progressPct = 0
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
progressPct = Math.min(((Date.now()-dispatch.startMs)/dispatch.durationMs)*100,100)
|
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)}%`
|
|
||||||
} else if (otium) {
|
} else if (otium) {
|
||||||
progressPct = Math.min(((Date.now()-otium.startMs)/OTIUM_DURATION_MS)*100,100)
|
progressPct = Math.min(((Date.now()-otium.startMs)/OTIUM_DURATION_MS)*100, 100)
|
||||||
progressLabel = 'resting...'
|
|
||||||
progressSub = `${Math.round(progressPct)}%`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section data
|
// Section data — active venture
|
||||||
const activeVentureItems = dispatch ? (() => {
|
const activeVentureItems = dispatch ? (() => {
|
||||||
const route = ROUTES.find(r=>r.id===dispatch.routeId)
|
const route = ROUTES.find(r => r.id === dispatch.routeId)
|
||||||
return [
|
return [
|
||||||
{key:'Route', value:`${WAYPOINTS[route.from].name} → ${WAYPOINTS[route.to].name}`, cls:''},
|
{ key: 'Route', value: `${WAYPOINTS[route.from].name} → ${WAYPOINTS[route.to].name}`, cls: '' },
|
||||||
{key:'Cargo', value:route.goods, cls:''},
|
{ key: 'Cargo', value: route.goods, cls: '' },
|
||||||
{key:'Progress', value:`${Math.round(progressPct)}%`, cls:'good'},
|
{ key: 'Progress', value: `${Math.round(progressPct)}%`, cls: 'good' },
|
||||||
{key:'Net', value:`+${route.profit-route.cost} dn expected`, cls:''},
|
{ key: 'Net', value: `+${route.profit - route.cost} dn expected`, cls: '' },
|
||||||
]
|
]
|
||||||
})() : otium ? [
|
})() : otium ? [
|
||||||
{key:'Status', value:'Otium in progress', cls:'warn'},
|
{ key: 'Status', value: 'Otium in progress', cls: 'warn' },
|
||||||
{key:'Progress', value:`${Math.round(progressPct)}%`, cls:''},
|
{ key: 'Progress', value: `${Math.round(progressPct)}%`, cls: '' },
|
||||||
{key:'Cost', value:`−${OTIUM_CYCLE_TOTAL_DN} dn`, cls:'bad'},
|
{ key: 'Cost', value: `−${OTIUM_CYCLE_TOTAL_DN} dn`, cls: 'bad' },
|
||||||
] : [{key:'Status', value:'No galley at sea', cls:''}]
|
] : [{ key: 'Status', value: 'No galley at sea', cls: '' }]
|
||||||
|
|
||||||
|
// Route list — fix: pass route.id to isRouteUnlocked
|
||||||
const routeItems = ROUTES.map(route => {
|
const routeItems = ROUTES.map(route => {
|
||||||
const unlocked = isRouteUnlocked(state, route)
|
const unlocked = isRouteUnlocked(state, route.id)
|
||||||
const vectura = Math.round(route.cost*0.60*10)/10
|
const vectura = Math.round(route.cost * 0.60 * 10) / 10
|
||||||
const portoria = Math.round(route.cost*0.25*10)/10
|
const portoria = Math.round(route.cost * 0.25 * 10) / 10
|
||||||
const other = Math.round((route.cost-vectura-portoria)*10)/10
|
const other = Math.round((route.cost - vectura - portoria) * 10) / 10
|
||||||
return {
|
return {
|
||||||
id: route.id,
|
id: route.id,
|
||||||
name: `${WAYPOINTS[route.from].name} → ${WAYPOINTS[route.to].name}`,
|
name: `${WAYPOINTS[route.from].name} → ${WAYPOINTS[route.to].name}`,
|
||||||
goods: route.goods,
|
goods: route.goods,
|
||||||
mode: (route.id==='grain'||route.id==='linen')?'sea':'road',
|
mode: (route.id === 'grain' || route.id === 'linen') ? 'sea' : 'road',
|
||||||
days: Math.round(route.duration_ms/3000),
|
days: Math.round(route.duration_ms / 3000),
|
||||||
cost: route.cost,
|
cost: route.cost,
|
||||||
profit: route.profit,
|
profit: route.profit,
|
||||||
vectura, portoria, other,
|
vectura, portoria, other,
|
||||||
locked: !unlocked,
|
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 chapter = getChapter(state.den, state.aut)
|
||||||
const standingItems = [
|
const standingItems = [
|
||||||
{key:'Denarii', value:`${Math.floor(state.den)} dn`, cls:state.den>100?'good':'warn'},
|
{ key: 'Denarii', value: `${Math.floor(state.den)} dn`, cls: state.den > 100 ? 'good' : 'warn' },
|
||||||
{key:'Auctoritas', value:`${Math.floor(state.aut)}`, cls:''},
|
{ 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: '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: 'Dispatches', value: `${state.dispatches || 0} complete`, cls: '' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const expenditureItems = [
|
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: '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: '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: '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 : [{
|
const journalEntries = (journal.length > 0 ? journal : [{
|
||||||
day:'Day 1 · Ostia',
|
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.',
|
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}))
|
}]).map(e => ({ date: e.day, text: e.text }))
|
||||||
|
|
||||||
const actionButtons = [
|
const actionButtons = [
|
||||||
{label:'Dispatch galley', style:'otivm-btn-dispatch', action:'dispatch', disabled:busy||!selectedRoute},
|
{ label: 'Dispatch galley', style: 'otivm-btn-dispatch', action: 'dispatch', disabled: busy || !selectedRoute },
|
||||||
{label:'Take otium', style:'otivm-btn-otium', action:'otium', disabled:busy},
|
{ label: 'Take otium', style: 'otivm-btn-otium', action: 'otium', disabled: busy },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message && (
|
{message && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position:'fixed',bottom:'20px',right:'20px',
|
position: 'fixed', bottom: '20px', right: '20px',
|
||||||
background:'var(--otivm-ink)',color:'var(--otivm-parch)',
|
background: 'var(--otivm-ink)', color: 'var(--otivm-parch)',
|
||||||
padding:'10px 18px',borderRadius:'3px',
|
padding: '10px 18px', borderRadius: '3px',
|
||||||
fontSize:'0.8rem',fontStyle:'italic',zIndex:300,
|
fontSize: '0.8rem', fontStyle: 'italic', zIndex: 300,
|
||||||
gridColumn:'1/-1',
|
|
||||||
}}>
|
}}>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user