Files
otivm/src/gameState.js

148 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 01 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)
}