import { useState, useEffect, useRef } from 'react' import { ROUTES, WAYPOINTS, OTIUM_DURATION_MS } from '../constants.js' import { applyDispatch, applyDispatchCost, applyOtium, getNewJournalEntry, getSeenJournalEntries, isRouteUnlocked, getChapter, } from '../gameState.js' const CHAPTERS = ['ostia', 'capua', 'brundisium', 'carthago', 'alexandria'] export default function Game({ state, onStateChange, onNewGame }) { const [selectedRoute, setSelectedRoute] = useState(null) const [dispatch, setDispatch] = useState(null) 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) function showMessage(text, dur = 3500) { setMessage(text) clearTimeout(msgRef.current) msgRef.current = setTimeout(() => setMessage(''), dur) } useEffect(() => { tickRef.current = setInterval(() => { const now = Date.now() if (dispatch) { const elapsed = now - dispatch.startMs if (elapsed >= dispatch.durationMs) { setDispatch(null) const entry = getNewJournalEntry(state, dispatch.routeId) 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) } setSelectedRoute(null) onStateChange(newState) } } if (otium) { const elapsed = now - otium.startMs if (elapsed >= OTIUM_DURATION_MS) { setOtium(null) const newState = applyOtium(state) const gain = newState.aut - state.aut showMessage(`Otium complete. +${gain} auctoritas.`) onStateChange(newState) } } }, 250) return () => clearInterval(tickRef.current) }, [dispatch, otium, state, onStateChange]) function handleSelectRoute(routeId) { if (dispatch || otium) return if (!isRouteUnlocked(state, routeId)) return setSelectedRoute((prev) => prev === routeId ? null : routeId) } function handleDispatch() { if (!selectedRoute) { showMessage('Select a trade route first.'); return } if (dispatch) { showMessage('A galley is already at sea.'); return } if (otium) { showMessage('Finish your otium first.'); return } const route = ROUTES.find((r) => r.id === selectedRoute) if (state.den < route.cost) { showMessage('Not enough denarii for this voyage.'); return } const newState = applyDispatchCost(state, selectedRoute) onStateChange(newState) setDispatch({ routeId: selectedRoute, startMs: Date.now(), durationMs: route.duration_ms }) showMessage(`Galley dispatched on ${route.from} → ${route.to}.`) } function handleOtium() { if (dispatch) { showMessage('Your galley is at sea. Wait for it to return.'); return } if (otium) return setOtium({ startMs: Date.now() }) showMessage('You rest. The harbour sounds fade.') } function handleNewGame() { if (!window.confirm('Abandon this ledger and begin a new one?')) return onNewGame() } let progressPct = 0 let progressLabel = '' let progressSub = '' let isOtium = false if (dispatch) { const elapsed = Date.now() - dispatch.startMs progressPct = Math.min((elapsed / 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) { const elapsed = Date.now() - otium.startMs progressPct = Math.min((elapsed / OTIUM_DURATION_MS) * 100, 100) progressLabel = 'resting...' progressSub = `${Math.round(progressPct)}%` isOtium = true } const chapter = getChapter(state.den, state.aut) const currentLocation = WAYPOINTS[CHAPTERS[chapter - 1]] const busy = !!dispatch || !!otium return (
mercator romanus · anno DCCXL ab urbe condita
Trade routes
Merchant's journal