15 changed files with 600 additions and 76 deletions
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
/** |
||||
* GameStateMachine - Centralized state management |
||||
* Manages all game states and transitions without adding new functionality |
||||
*/ |
||||
export class GameStateMachine { |
||||
constructor(gameRoot) { |
||||
this.gameRoot = gameRoot; |
||||
this.currentState = null; |
||||
this.states = new Map(); |
||||
this.stateHistory = []; // For debugging
|
||||
} |
||||
|
||||
/** |
||||
* Register a state with the state machine |
||||
* @param {string} name - State name |
||||
* @param {GameState} state - State instance |
||||
*/ |
||||
registerState(name, state) { |
||||
this.states.set(name, state); |
||||
} |
||||
|
||||
/** |
||||
* Get the current state |
||||
* @returns {GameState|null} |
||||
*/ |
||||
getCurrentState() { |
||||
return this.currentState; |
||||
} |
||||
|
||||
/** |
||||
* Get current state name |
||||
* @returns {string|null} |
||||
*/ |
||||
getCurrentStateName() { |
||||
return this.currentState?.name || null; |
||||
} |
||||
|
||||
/** |
||||
* Transition to a new state |
||||
* @param {string} stateName - Name of the state to transition to |
||||
* @param {Object} transitionData - Optional data for the transition |
||||
*/ |
||||
async setState(stateName, transitionData = {}) { |
||||
const newState = this.states.get(stateName); |
||||
if (!newState) { |
||||
console.error(`State '${stateName}' not found`); |
||||
return false; |
||||
} |
||||
|
||||
const previousState = this.currentState; |
||||
|
||||
// Exit current state
|
||||
if (previousState) { |
||||
await previousState.exit(this.gameRoot, newState); |
||||
} |
||||
|
||||
// Update current state
|
||||
this.currentState = newState; |
||||
|
||||
// Add to history for debugging
|
||||
this.stateHistory.push({ |
||||
from: previousState?.name || 'none', |
||||
to: stateName, |
||||
timestamp: Date.now(), |
||||
data: transitionData |
||||
}); |
||||
|
||||
// Keep history reasonable size
|
||||
if (this.stateHistory.length > 50) { |
||||
this.stateHistory.shift(); |
||||
} |
||||
|
||||
// Enter new state
|
||||
await newState.enter(this.gameRoot, previousState); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Render the current state |
||||
*/ |
||||
async render() { |
||||
if (this.currentState) { |
||||
await this.currentState.render(this.gameRoot); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get state data for saving |
||||
*/ |
||||
getSaveData() { |
||||
const data = { |
||||
currentStateName: this.getCurrentStateName(), |
||||
stateHistory: this.stateHistory.slice(-10) // Save last 10 for debugging
|
||||
}; |
||||
|
||||
// Get state-specific save data
|
||||
if (this.currentState) { |
||||
data.stateData = this.currentState.getSaveData(this.gameRoot); |
||||
} |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** |
||||
* Restore state from save data |
||||
* @param {Object} saveData - The saved state data |
||||
*/ |
||||
async restoreFromSave(saveData) { |
||||
if (!saveData.currentStateName) { |
||||
console.warn('No state name in save data'); |
||||
return false; |
||||
} |
||||
|
||||
const success = await this.setState(saveData.currentStateName); |
||||
if (success && this.currentState && saveData.stateData) { |
||||
this.currentState.restoreFromSave(this.gameRoot, saveData.stateData); |
||||
} |
||||
|
||||
// Restore history if available
|
||||
if (saveData.stateHistory) { |
||||
this.stateHistory = saveData.stateHistory; |
||||
} |
||||
|
||||
return success; |
||||
} |
||||
|
||||
/** |
||||
* Get state transition history (for debugging) |
||||
*/ |
||||
getHistory() { |
||||
return this.stateHistory.slice(); |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderBattle } from '../../ui/render.js'; |
||||
import { createBattle } from '../battle.js'; |
||||
|
||||
/** |
||||
* BattleState - Handles combat |
||||
* Preserves exact existing functionality from createBattle() and renderBattle() |
||||
*/ |
||||
export class BattleState extends GameState { |
||||
constructor() { |
||||
super('BATTLE'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Set battle flag (preserves existing behavior)
|
||||
gameRoot._battleInProgress = true; |
||||
|
||||
// If we don't have an enemy yet, we need to create the battle
|
||||
// This happens when transitioning from map to battle
|
||||
if (!gameRoot.enemy) { |
||||
const node = gameRoot.map.nodes.find(n => n.id === gameRoot.nodeId); |
||||
if (node && node.enemy) { |
||||
createBattle(gameRoot, node.enemy); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async exit(gameRoot, nextState = null) { |
||||
// Clear battle flag when leaving battle
|
||||
gameRoot._battleInProgress = false; |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
await renderBattle(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId, |
||||
battleInProgress: gameRoot._battleInProgress, |
||||
enemy: gameRoot.enemy, |
||||
flags: gameRoot.flags, |
||||
lastCard: gameRoot.lastCard |
||||
}; |
||||
} |
||||
|
||||
restoreFromSave(gameRoot, saveData) { |
||||
if (saveData.battleInProgress !== undefined) { |
||||
gameRoot._battleInProgress = saveData.battleInProgress; |
||||
} |
||||
if (saveData.enemy) gameRoot.enemy = saveData.enemy; |
||||
if (saveData.flags) gameRoot.flags = saveData.flags; |
||||
if (saveData.lastCard) gameRoot.lastCard = saveData.lastCard; |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderLose } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* DefeatState - Handles defeat screen |
||||
* Preserves exact existing functionality from renderLose() |
||||
*/ |
||||
export class DefeatState extends GameState { |
||||
constructor() { |
||||
super('DEFEAT'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
await renderLose(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderEvent } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* EventState - Handles random events |
||||
* Preserves exact existing functionality from renderEvent() |
||||
*/ |
||||
export class EventState extends GameState { |
||||
constructor() { |
||||
super('EVENT'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Save when entering event (preserves existing behavior)
|
||||
gameRoot.save(); |
||||
|
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async exit(gameRoot, nextState = null) { |
||||
// Clear event-specific state when leaving
|
||||
gameRoot.currentEvent = null; |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
renderEvent(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId, |
||||
currentEvent: gameRoot.currentEvent |
||||
}; |
||||
} |
||||
|
||||
restoreFromSave(gameRoot, saveData) { |
||||
if (saveData.currentEvent) gameRoot.currentEvent = saveData.currentEvent; |
||||
} |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/** |
||||
* Base GameState class for the State pattern |
||||
* All game states inherit from this base class |
||||
*/ |
||||
export class GameState { |
||||
constructor(name) { |
||||
this.name = name; |
||||
} |
||||
|
||||
/** |
||||
* Called when entering this state |
||||
* @param {Object} gameRoot - The game root object |
||||
* @param {Object} previousState - The previous state (optional) |
||||
*/ |
||||
async enter(gameRoot, previousState = null) { |
||||
// Override in subclasses
|
||||
} |
||||
|
||||
/** |
||||
* Called when exiting this state |
||||
* @param {Object} gameRoot - The game root object |
||||
* @param {Object} nextState - The next state (optional) |
||||
*/ |
||||
async exit(gameRoot, nextState = null) { |
||||
// Override in subclasses
|
||||
} |
||||
|
||||
/** |
||||
* Handle state-specific rendering |
||||
* @param {Object} gameRoot - The game root object |
||||
*/ |
||||
async render(gameRoot) { |
||||
// Override in subclasses
|
||||
throw new Error(`render() not implemented for state: ${this.name}`); |
||||
} |
||||
|
||||
/** |
||||
* Get state-specific data for saving |
||||
* @param {Object} gameRoot - The game root object |
||||
*/ |
||||
getSaveData(gameRoot) { |
||||
return { |
||||
stateName: this.name |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Restore state-specific data from save |
||||
* @param {Object} gameRoot - The game root object |
||||
* @param {Object} saveData - The saved data |
||||
*/ |
||||
restoreFromSave(gameRoot, saveData) { |
||||
// Override in subclasses if needed
|
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderMap } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* MapState - Handles map navigation |
||||
* Preserves exact existing functionality from root.go() and renderMap() |
||||
*/ |
||||
export class MapState extends GameState { |
||||
constructor() { |
||||
super('MAP'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Clear battle-specific state when entering map
|
||||
gameRoot.enemy = null; |
||||
gameRoot._battleInProgress = false; |
||||
|
||||
// Save when entering map (preserves existing behavior)
|
||||
gameRoot.save(); |
||||
|
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
await renderMap(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId, |
||||
currentAct: gameRoot.currentAct, |
||||
completedNodes: gameRoot.completedNodes |
||||
}; |
||||
} |
||||
|
||||
restoreFromSave(gameRoot, saveData) { |
||||
if (saveData.nodeId) gameRoot.nodeId = saveData.nodeId; |
||||
if (saveData.currentAct) gameRoot.currentAct = saveData.currentAct; |
||||
if (saveData.completedNodes) gameRoot.completedNodes = saveData.completedNodes; |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderRelicSelection } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* RelicSelectionState - Handles starting relic choice |
||||
* Preserves exact existing functionality from renderRelicSelection() |
||||
*/ |
||||
export class RelicSelectionState extends GameState { |
||||
constructor() { |
||||
super('RELIC_SELECTION'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
renderRelicSelection(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot) |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderRest } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* RestState - Handles rest/upgrade interactions |
||||
* Preserves exact existing functionality from renderRest() |
||||
*/ |
||||
export class RestState extends GameState { |
||||
constructor() { |
||||
super('REST'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Save when entering rest (preserves existing behavior)
|
||||
gameRoot.save(); |
||||
|
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
await renderRest(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderShop } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* ShopState - Handles shop interactions |
||||
* Preserves exact existing functionality from renderShop() |
||||
*/ |
||||
export class ShopState extends GameState { |
||||
constructor() { |
||||
super('SHOP'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Save when entering shop (preserves existing behavior)
|
||||
gameRoot.save(); |
||||
|
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async exit(gameRoot, nextState = null) { |
||||
// Clear shop-specific state when leaving
|
||||
gameRoot.currentShopCards = null; |
||||
gameRoot.currentShopRelic = null; |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
renderShop(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId, |
||||
currentShopCards: gameRoot.currentShopCards, |
||||
currentShopRelic: gameRoot.currentShopRelic |
||||
}; |
||||
} |
||||
|
||||
restoreFromSave(gameRoot, saveData) { |
||||
if (saveData.currentShopCards) gameRoot.currentShopCards = saveData.currentShopCards; |
||||
if (saveData.currentShopRelic) gameRoot.currentShopRelic = saveData.currentShopRelic; |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import { GameState } from './GameState.js'; |
||||
import { renderWin } from '../../ui/render.js'; |
||||
|
||||
/** |
||||
* VictoryState - Handles victory screen |
||||
* Preserves exact existing functionality from renderWin() |
||||
*/ |
||||
export class VictoryState extends GameState { |
||||
constructor() { |
||||
super('VICTORY'); |
||||
} |
||||
|
||||
async enter(gameRoot, previousState = null) { |
||||
// Trigger initial render when entering the state
|
||||
await gameRoot.render(); |
||||
} |
||||
|
||||
async render(gameRoot) { |
||||
await renderWin(gameRoot); |
||||
} |
||||
|
||||
getSaveData(gameRoot) { |
||||
return { |
||||
...super.getSaveData(gameRoot), |
||||
nodeId: gameRoot.nodeId |
||||
}; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue