148 lines
4.5 KiB
JavaScript
148 lines
4.5 KiB
JavaScript
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)
|
||
}
|