import { ROUTES, JOURNAL, INITIAL_STATE, OTIUM_BASE_AUT, } from './constants.js' // Returns a fresh game state with a generated token export function createState(token) { return { ...INITIAL_STATE, token, created_at: new Date().toISOString(), } } // Returns current chapter based on den and aut thresholds export function getChapter(den, aut) { if (aut >= 30 && den >= 800) return 5 if (aut >= 15 && den >= 350) return 4 if (aut >= 5 && den >= 120) return 3 if (den >= 40) return 2 return 1 } // Returns routes available to this state (unlocked, not locked) export function getUnlockedRoutes(state) { return ROUTES.filter( (r) => state.den >= r.unlock_den && state.aut >= r.unlock_aut ) } // Returns true if a route is unlocked export function isRouteUnlocked(state, routeId) { const r = ROUTES.find((x) => x.id === routeId) if (!r) return false return state.den >= r.unlock_den && state.aut >= r.unlock_aut } // Returns a 0–1 progress float for a dispatched galley along its route. // 0 = just departed, 1 = returned. Returns null if no active dispatch. // This function is pure — it takes a snapshot of active_dispatch and a // current timestamp. OTIVM-IV will feed real duration_ms values. // OTIVM-VII will map the progress float onto H3 waypoints along the route. export function galleyProgress(active_dispatch, now_ms) { if (!active_dispatch) return null const { started_utc, duration_ms } = active_dispatch const elapsed = now_ms - new Date(started_utc).getTime() return Math.min(elapsed / duration_ms, 1) } // Append an event to state — returns new state. // Internal helper used by apply* functions below. function appendEvent(state, type, route_id = null) { const event = { type, route_id, timestamp_utc: new Date().toISOString(), } return { ...state, events: [...(state.events || []), event], } } // Apply cost of launching a dispatch — returns new state or null if insufficient funds. // Sets active_dispatch so galleyProgress can track position. export function applyDispatchCost(state, routeId) { const r = ROUTES.find((x) => x.id === routeId) if (!r) return null if (state.den < r.cost) return null const withCost = { ...state, den: state.den - r.cost } const withDispatch = { ...withCost, active_dispatch: { route_id: routeId, started_utc: new Date().toISOString(), duration_ms: r.duration_ms, }, } return appendEvent(withDispatch, 'dispatch_start', routeId) } // Apply a completed dispatch — returns new state. // Clears active_dispatch on completion. export function applyDispatch(state, routeId) { const r = ROUTES.find((x) => x.id === routeId) if (!r) return state const route_dispatches = { ...state.route_dispatches, [routeId]: (state.route_dispatches[routeId] || 0) + 1, } const den = state.den + r.profit const dispatches = state.dispatches + 1 const prevChapter = state.chapter const chapter = getChapter(den, state.aut) // Check for new journal entry const dispatchCount = route_dispatches[routeId] const entries = JOURNAL[routeId] || [] const entry = entries.find((e) => e.dispatch === dispatchCount) const journal_seen = entry ? [...state.journal_seen, { routeId, dispatch: dispatchCount }] : state.journal_seen let next = { ...state, den, dispatches, chapter, route_dispatches, journal_seen, active_dispatch: null, } next = appendEvent(next, 'dispatch_complete', routeId) if (entry) next = appendEvent(next, 'journal_unlock', routeId) if (chapter > prevChapter) next = appendEvent(next, 'chapter_advance', null) return next } // Apply completed otium — returns new state export function applyOtium(state) { const gain = OTIUM_BASE_AUT + Math.floor(state.dispatches / 3) const aut = state.aut + gain const chapter = getChapter(state.den, aut) const next = { ...state, aut, chapter } return appendEvent(next, 'otium', null) } // Returns the journal entry to show for a completed dispatch, or null export function getNewJournalEntry(state, routeId) { const dispatchCount = (state.route_dispatches[routeId] || 0) const entries = JOURNAL[routeId] || [] return entries.find((e) => e.dispatch === dispatchCount) || null } // Returns all journal entries seen so far, in order seen export function getSeenJournalEntries(state) { return state.journal_seen.map(({ routeId, dispatch }) => { const entries = JOURNAL[routeId] || [] return entries.find((e) => e.dispatch === dispatch) }).filter(Boolean) }