diff --git a/src/Game.jsx b/src/Game.jsx new file mode 100644 index 0000000..baa1706 --- /dev/null +++ b/src/Game.jsx @@ -0,0 +1,263 @@ +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 }) { + const [selectedRoute, setSelectedRoute] = useState(null) + const [dispatch, setDispatch] = useState(null) // { routeId, startMs, durationMs } + const [otium, setOtium] = useState(null) // { startMs } + 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) + } + + // Game tick — 250ms + useEffect(() => { + tickRef.current = setInterval(() => { + const now = Date.now() + + if (dispatch) { + const elapsed = now - dispatch.startMs + if (elapsed >= dispatch.durationMs) { + // Dispatch complete + 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.') + } + + // Progress bar values + 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
+