From f7f4068713fcf6f53bb995775f5d4ffbe0805fe1 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Mon, 8 Sep 2025 00:26:10 -0700 Subject: [PATCH] implement state machine --- src/commands/CommandInvoker.js | 1 - src/engine/GameStateMachine.js | 134 +++++++++++++++++++++++ src/engine/events.js | 23 ++-- src/engine/states/BattleState.js | 56 ++++++++++ src/engine/states/DefeatState.js | 28 +++++ src/engine/states/EventState.js | 41 +++++++ src/engine/states/GameState.js | 55 ++++++++++ src/engine/states/MapState.js | 43 ++++++++ src/engine/states/RelicSelectionState.js | 27 +++++ src/engine/states/RestState.js | 31 ++++++ src/engine/states/ShopState.js | 44 ++++++++ src/engine/states/VictoryState.js | 28 +++++ src/input/InputManager.js | 7 +- src/main.js | 129 ++++++++++++++-------- src/ui/render.js | 29 +++-- 15 files changed, 600 insertions(+), 76 deletions(-) create mode 100644 src/engine/GameStateMachine.js create mode 100644 src/engine/states/BattleState.js create mode 100644 src/engine/states/DefeatState.js create mode 100644 src/engine/states/EventState.js create mode 100644 src/engine/states/GameState.js create mode 100644 src/engine/states/MapState.js create mode 100644 src/engine/states/RelicSelectionState.js create mode 100644 src/engine/states/RestState.js create mode 100644 src/engine/states/ShopState.js create mode 100644 src/engine/states/VictoryState.js diff --git a/src/commands/CommandInvoker.js b/src/commands/CommandInvoker.js index c0c532c..9a638dd 100644 --- a/src/commands/CommandInvoker.js +++ b/src/commands/CommandInvoker.js @@ -27,7 +27,6 @@ export class CommandInvoker { } command.executed = true; - console.log(`Executed: ${command.getDescription()}`); } return success; diff --git a/src/engine/GameStateMachine.js b/src/engine/GameStateMachine.js new file mode 100644 index 0000000..dfa232c --- /dev/null +++ b/src/engine/GameStateMachine.js @@ -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(); + } +} diff --git a/src/engine/events.js b/src/engine/events.js index 6a08d5d..2288a66 100644 --- a/src/engine/events.js +++ b/src/engine/events.js @@ -191,9 +191,9 @@ export class EventHandler { // Reset button const resetBtn = this.root.app.querySelector("[data-reset]"); if (resetBtn) { - this.on(resetBtn, "click", () => { + this.on(resetBtn, "click", async () => { this.root.clearSave(); - this.root.reset(); + await this.root.reset(); }); } @@ -330,19 +330,12 @@ export class EventHandler { setupEventEvents(event) { this.switchScreen('event'); - this.root.app.querySelectorAll("[data-choice]").forEach(btn => { - this.on(btn, "click", () => { - const idx = parseInt(btn.dataset.choice, 10); - event.choices[idx].effect(); - this.root.afterNode(); - }); - }); - - // Keyboard shortcuts for event choices + // Event choice handlers are managed by InputManager + // Just set up keyboard shortcuts here for (let i = 1; i <= event.choices.length; i++) { - this.addKeyHandler(i.toString(), () => { + this.addKeyHandler(i.toString(), async () => { event.choices[i - 1].effect(); - this.root.afterNode(); + await this.root.afterNode(); }, `Event Choice ${i}`); } } @@ -388,7 +381,7 @@ export class EventHandler { const menuBtn = this.root.app.querySelector("[data-menu]"); if (replayBtn) { - this.on(replayBtn, "click", () => this.root.reset()); + this.on(replayBtn, "click", async () => await this.root.reset()); } if (restartAct2Btn) { @@ -403,7 +396,7 @@ export class EventHandler { } if (menuBtn) { - this.on(menuBtn, "click", () => this.root.reset()); + this.on(menuBtn, "click", async () => await this.root.reset()); } // Keyboard shortcuts diff --git a/src/engine/states/BattleState.js b/src/engine/states/BattleState.js new file mode 100644 index 0000000..8369344 --- /dev/null +++ b/src/engine/states/BattleState.js @@ -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; + } +} diff --git a/src/engine/states/DefeatState.js b/src/engine/states/DefeatState.js new file mode 100644 index 0000000..76318a2 --- /dev/null +++ b/src/engine/states/DefeatState.js @@ -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 + }; + } +} diff --git a/src/engine/states/EventState.js b/src/engine/states/EventState.js new file mode 100644 index 0000000..8e21daa --- /dev/null +++ b/src/engine/states/EventState.js @@ -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; + } +} diff --git a/src/engine/states/GameState.js b/src/engine/states/GameState.js new file mode 100644 index 0000000..e4f0d00 --- /dev/null +++ b/src/engine/states/GameState.js @@ -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 + } +} diff --git a/src/engine/states/MapState.js b/src/engine/states/MapState.js new file mode 100644 index 0000000..9891579 --- /dev/null +++ b/src/engine/states/MapState.js @@ -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; + } +} diff --git a/src/engine/states/RelicSelectionState.js b/src/engine/states/RelicSelectionState.js new file mode 100644 index 0000000..a7542d9 --- /dev/null +++ b/src/engine/states/RelicSelectionState.js @@ -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) + }; + } +} diff --git a/src/engine/states/RestState.js b/src/engine/states/RestState.js new file mode 100644 index 0000000..efc51b4 --- /dev/null +++ b/src/engine/states/RestState.js @@ -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 + }; + } +} diff --git a/src/engine/states/ShopState.js b/src/engine/states/ShopState.js new file mode 100644 index 0000000..a5d017d --- /dev/null +++ b/src/engine/states/ShopState.js @@ -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; + } +} diff --git a/src/engine/states/VictoryState.js b/src/engine/states/VictoryState.js new file mode 100644 index 0000000..38a33d4 --- /dev/null +++ b/src/engine/states/VictoryState.js @@ -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 + }; + } +} diff --git a/src/input/InputManager.js b/src/input/InputManager.js index b3deebe..8ff7593 100644 --- a/src/input/InputManager.js +++ b/src/input/InputManager.js @@ -3,8 +3,6 @@ * * This class consolidates ALL event listeners from the render functions * into one place while maintaining exact same functionality. - * - * Following Nystrom's Input Handling patterns from Game Programming Patterns */ import { PlayCardCommand } from '../commands/PlayCardCommand.js'; @@ -220,12 +218,13 @@ export class InputManager { /** * Handle event choice clicks */ - handleEventChoice(element, event) { + async handleEventChoice(element, event) { const idx = parseInt(element.dataset.choice, 10); + // Get the current event from the root (this will need to be accessible) if (this.root.currentEvent && this.root.currentEvent.choices[idx]) { this.root.currentEvent.choices[idx].effect(); - this.root.afterNode(); + await this.root.afterNode(); } } diff --git a/src/main.js b/src/main.js index be9d822..239906f 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,15 @@ import { createBattle, startPlayerTurn, playCard, endTurn, makeBattleContext, at import { renderBattle, renderMap, renderReward, renderRest, renderShop, renderWin, renderLose, renderEvent, renderRelicSelection, renderUpgrade, updateCardSelection, showDamageNumber, renderCodeReviewSelection } from "./ui/render.js"; import { InputManager } from "./input/InputManager.js"; import { CommandInvoker } from "./commands/CommandInvoker.js"; +import { GameStateMachine } from "./engine/GameStateMachine.js"; +import { MapState } from "./engine/states/MapState.js"; +import { BattleState } from "./engine/states/BattleState.js"; +import { ShopState } from "./engine/states/ShopState.js"; +import { RestState } from "./engine/states/RestState.js"; +import { EventState } from "./engine/states/EventState.js"; +import { VictoryState } from "./engine/states/VictoryState.js"; +import { DefeatState } from "./engine/states/DefeatState.js"; +import { RelicSelectionState } from "./engine/states/RelicSelectionState.js"; const app = document.getElementById("app"); @@ -20,6 +29,7 @@ const root = { enemy: null, inputManager: null, // Will be initialized later commandInvoker: new CommandInvoker(), + stateMachine: null, // Will be initialized below currentEvent: null, // For event handling currentShopCards: null, // For shop handling currentShopRelic: null, // For shop relic handling @@ -27,7 +37,14 @@ const root = { _codeReviewCallback: null, // For code review completion log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, - async render() { await renderBattle(this); }, + async render() { + if (this.stateMachine) { + await this.stateMachine.render(); + } else { + // Fallback for initialization + await renderBattle(this); + } + }, play(i) { const battleCtx = makeBattleContext(this); playCard(battleCtx, i); @@ -44,23 +61,17 @@ const root = { const node = this.map.nodes.find(n => n.id === nextId); if (!node) return; + // Use state machine for transitions if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { - - this._battleInProgress = true; - createBattle(this, node.enemy); - await renderBattle(this); - } else { - - this.save(); - if (node.kind === "rest") { - await renderRest(this); - } else if (node.kind === "shop") { - renderShop(this); - } else if (node.kind === "event") { - renderEvent(this); - } else if (node.kind === "start") { - await renderMap(this); - } + await this.stateMachine.setState('BATTLE'); + } else if (node.kind === "rest") { + await this.stateMachine.setState('REST'); + } else if (node.kind === "shop") { + await this.stateMachine.setState('SHOP'); + } else if (node.kind === "event") { + await this.stateMachine.setState('EVENT'); + } else if (node.kind === "start") { + await this.stateMachine.setState('MAP'); } }, @@ -69,8 +80,8 @@ const root = { this.completedNodes.push(this.nodeId); } - const node = this.map.nodes.find(n => n.id === this.nodeId); + if (node.kind === "battle" || node.kind === "elite") { const choices = pickCards(3); this._pendingChoices = choices; @@ -78,10 +89,11 @@ const root = { return; } if (node.kind === "boss") { - await renderWin(this); return; + await this.stateMachine.setState('VICTORY'); + return; } - await renderMap(this); + await this.stateMachine.setState('MAP'); }, async takeReward(idx) { @@ -92,12 +104,12 @@ const root = { } this._pendingChoices = null; this.save(); - await renderMap(this); + await this.stateMachine.setState('MAP'); }, async skipReward() { this._pendingChoices = null; this.save(); - await renderMap(this); + await this.stateMachine.setState('MAP'); }, async onWin() { @@ -111,9 +123,11 @@ const root = { this._battleInProgress = false; const node = this.map.nodes.find(n => n.id === this.nodeId); + if (node.kind === "boss") { // Check if there's a next act const nextAct = this.currentAct === "act1" ? "act2" : null; + if (nextAct && MAPS[nextAct]) { // Advance to next act this.currentAct = nextAct; @@ -128,27 +142,27 @@ const root = { } this.save(); - await renderMap(this); + await this.stateMachine.setState('MAP'); } else { // Final victory this.save(); // Save progress before clearing on victory this.clearSave(); // Clear save on victory - await renderWin(this); + await this.stateMachine.setState('VICTORY'); } } else { this.save(); - this.afterNode(); + await this.afterNode(); } }, async onLose() { this._battleInProgress = false; this.clearSave(); // Clear save on defeat - await renderLose(this); + await this.stateMachine.setState('DEFEAT'); }, - reset() { + async reset() { this.logs = []; this.player = makePlayer(); initDeck(this.player); @@ -157,13 +171,13 @@ const root = { this.nodeId = "n1"; this.completedNodes = []; - renderRelicSelection(this); + await this.stateMachine.setState('RELIC_SELECTION'); }, async selectStartingRelic(relicId) { attachRelics(this, [relicId]); this.save(); - await renderMap(this); + await this.stateMachine.setState('MAP'); }, save() { @@ -176,6 +190,7 @@ const root = { completedNodes: this.completedNodes, logs: this.logs.slice(-50), // Keep last 50 logs battleInProgress: this._battleInProgress || false, + stateMachine: this.stateMachine ? this.stateMachine.getSaveData() : null, timestamp: Date.now() }; localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); @@ -300,6 +315,11 @@ const root = { this.logs = Array.isArray(data.logs) ? data.logs : []; this._battleInProgress = Boolean(data.battleInProgress); + // Restore state machine state + if (data.stateMachine && this.stateMachine) { + this.stateMachine.restoreFromSave(data.stateMachine); + } + this.restoreCardEffects(); this.log('Game loaded from save.'); @@ -343,6 +363,21 @@ const root = { } }; +// Initialize State Machine +try { + root.stateMachine = new GameStateMachine(root); + root.stateMachine.registerState('MAP', new MapState()); + root.stateMachine.registerState('BATTLE', new BattleState()); + root.stateMachine.registerState('SHOP', new ShopState()); + root.stateMachine.registerState('REST', new RestState()); + root.stateMachine.registerState('EVENT', new EventState()); + root.stateMachine.registerState('VICTORY', new VictoryState()); + root.stateMachine.registerState('DEFEAT', new DefeatState()); + root.stateMachine.registerState('RELIC_SELECTION', new RelicSelectionState()); +} catch (error) { + console.error('Error initializing state machine:', error); +} + function pickCards(n) { const ids = shuffle(CARD_POOL.slice()).slice(0, n); return ids.map(id => CARDS[id]); @@ -394,29 +429,29 @@ async function initializeGame() { switch (screenParam.toLowerCase()) { case 'victory': case 'win': - await renderWin(root); + await root.stateMachine.setState('VICTORY'); return; case 'defeat': case 'lose': - await renderLose(root); + await root.stateMachine.setState('DEFEAT'); return; case 'map': - await renderMap(root); + await root.stateMachine.setState('MAP'); return; case 'shop': - renderShop(root); + await root.stateMachine.setState('SHOP'); return; case 'rest': - await renderRest(root); + await root.stateMachine.setState('REST'); return; case 'event': - renderEvent(root); + await root.stateMachine.setState('EVENT'); return; case 'battle': - root.go('n2'); // Battle node + root.go('n2'); // Battle node (uses state machine internally) return; case 'upgrade': - await renderRest(root); + await root.stateMachine.setState('REST'); setTimeout(() => { const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); if (upgradeBtn) upgradeBtn.click(); @@ -424,7 +459,7 @@ async function initializeGame() { return; case 'relic': case 'relics': - renderRelicSelection(root); + await root.stateMachine.setState('RELIC_SELECTION'); return; default: console.warn(`Unknown screen: ${screenParam}. Loading normal game.`); @@ -541,19 +576,23 @@ async function loadNormalGame() { } else { // Battle state inconsistent, go to map root._battleInProgress = false; - await renderMap(root); + await root.stateMachine.setState('MAP'); } } else { - await renderMap(root); + await root.stateMachine.setState('MAP'); } } else { - root.reset(); + await root.reset(); } } // Initialize InputManager -root.inputManager = new InputManager(root); -root.inputManager.initGlobalListeners(); +try { + root.inputManager = new InputManager(root); + root.inputManager.initGlobalListeners(); +} catch (error) { + console.error('Error initializing InputManager:', error); +} // Make modules available globally for InputManager window.gameModules = { @@ -561,5 +600,7 @@ window.gameModules = { render: { renderMap, renderUpgrade, updateCardSelection, renderCodeReviewSelection } }; -initializeGame(); +initializeGame().catch(error => { + console.error('Error during game initialization:', error); +}); diff --git a/src/ui/render.js b/src/ui/render.js index f52ede4..ce8d7f3 100644 --- a/src/ui/render.js +++ b/src/ui/render.js @@ -11,7 +11,6 @@ function playSound(soundFile) { } export function showDamageNumber(damage, target, isPlayer = false) { - console.log('this is shown - damage number') const targetElement = isPlayer ? document.querySelector('.player-battle-zone') : document.querySelector('.enemy-battle-zone'); @@ -1125,9 +1124,15 @@ export function renderEvent(root) { icon: "assets/card-art/apple.png", risk: "high", effect: () => { - root.player.hp = Math.min(root.player.maxHp, root.player.hp + 15); + const oldHp = root.player.hp; + + root.player.hp += 15; + if (root.player.hp > root.player.maxHp) { + root.player.maxHp = root.player.hp; + } + root.player.deck.push("sugar_crash"); - root.log("Ate cake: +15 HP, added Sugar Crash curse"); + root.log(`Ate cake: +15 HP (${oldHp} → ${root.player.hp}), added Sugar Crash curse`); } }, { @@ -1136,7 +1141,10 @@ export function renderEvent(root) { icon: "assets/card-art/heart.png", risk: "low", effect: () => { - root.player.maxHp += 5; + root.player.maxHp += 8; + if (root.player.hp > root.player.maxHp) { + root.player.maxHp = root.player.hp; + } root.log("Small bite: +8 HP"); } }, @@ -1245,6 +1253,9 @@ export function renderEvent(root) { ]; const event = events[Math.floor(Math.random() * events.length)]; + + // Store the current event so other systems can access it + root.currentEvent = event; root.app.innerHTML = `
@@ -1296,13 +1307,7 @@ export function renderEvent(root) {
`; - root.app.querySelectorAll("[data-choice]").forEach(btn => { - btn.addEventListener("click", () => { - const idx = parseInt(btn.dataset.choice, 10); - event.choices[idx].effect(); - root.afterNode(); - }); - }); + // Event handlers are managed by InputManager - no need to add them here } export async function renderWin(root) { @@ -1402,7 +1407,7 @@ export async function renderWin(root) { `; - root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); + root.app.querySelector("[data-replay]").addEventListener("click", async () => await root.reset()); } export async function renderCodeReviewSelection(root, cards) {