feat(adventure): add state machine and navigation engine

This commit is contained in:
Alexander Brown
2026-03-26 17:41:21 -07:00
committed by DrJKL
parent fcb7d914f3
commit 62a34952f1
2 changed files with 151 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
import type { RoomDefinition, SaveState } from '@/types'
import { canEnterRoom } from '@/state/tags'
type NavigationResult =
| { allowed: true }
| { allowed: false; unmetTags: string[] }
function checkNavigation(
room: RoomDefinition,
save: SaveState
): NavigationResult {
if (canEnterRoom(room, save)) {
return { allowed: true }
}
const unmetTags = room.prerequisites.filter(
(tag) => !save.currentRun.conceptTags.includes(tag)
)
return { allowed: false, unmetTags }
}
function isRoomDiscovered(roomId: string, save: SaveState): boolean {
return save.currentRun.path.includes(roomId)
}
function isChallengeResolved(challengeId: string, save: SaveState): boolean {
return challengeId in save.currentRun.resolvedChallenges
}
function countResolvedChallenges(save: SaveState): number {
return Object.keys(save.currentRun.resolvedChallenges).length
}
export type { NavigationResult }
export {
checkNavigation,
countResolvedChallenges,
isChallengeResolved,
isRoomDiscovered
}

View File

@@ -0,0 +1,112 @@
import type {
ChallengeDefinition,
ChallengeResult,
GamePhase,
GameState,
SaveState
} from '@/types'
import { persistSave } from '@/state/gameState'
import { grantTags } from '@/state/tags'
type GameEventHandler = (state: GameState) => void
let currentState: GameState
let listeners: GameEventHandler[] = []
function initGameState(save: SaveState): void {
currentState = {
phase: 'exploring',
save
}
notify()
}
function getGameState(): GameState {
return currentState
}
function subscribe(handler: GameEventHandler): () => void {
listeners.push(handler)
return () => {
listeners = listeners.filter((l) => l !== handler)
}
}
function notify(): void {
for (const listener of listeners) {
listener(currentState)
}
}
function transition(phase: GamePhase, saveUpdates?: Partial<SaveState>): void {
const newSave = saveUpdates
? { ...currentState.save, ...saveUpdates }
: currentState.save
currentState = { phase, save: newSave }
persistSave(currentState.save)
notify()
}
function enterRoom(roomId: string): void {
const run = currentState.save.currentRun
const newPath = run.path.includes(roomId) ? run.path : [...run.path, roomId]
transition('exploring', {
currentRun: {
...run,
currentRoom: roomId,
path: newPath
}
})
}
function resolveChallenge(
challenge: ChallengeDefinition,
choiceKey: string
): void {
const choice = challenge.choices.find((c) => c.key === choiceKey)
if (!choice) return
const result: ChallengeResult = {
choiceKey,
rating: choice.rating,
tier: challenge.tier
}
let save = {
...currentState.save,
currentRun: {
...currentState.save.currentRun,
resolvedChallenges: {
...currentState.save.currentRun.resolvedChallenges,
[challenge.id]: result
},
insightEarned:
currentState.save.currentRun.insightEarned + choice.insightReward
}
}
save = grantTags(save, choice.tagsGranted)
transition('challenge-resolved', save)
}
function showEnding(): void {
transition('ending')
}
function resetForPrestige(newSave: SaveState): void {
transition('exploring', newSave)
}
export {
enterRoom,
getGameState,
initGameState,
resetForPrestige,
resolveChallenge,
showEnding,
subscribe,
transition
}