Browse Source

implement state machine

main
Stephanie Gredell 4 months ago
parent
commit
f7f4068713
  1. 1
      src/commands/CommandInvoker.js
  2. 134
      src/engine/GameStateMachine.js
  3. 23
      src/engine/events.js
  4. 56
      src/engine/states/BattleState.js
  5. 28
      src/engine/states/DefeatState.js
  6. 41
      src/engine/states/EventState.js
  7. 55
      src/engine/states/GameState.js
  8. 43
      src/engine/states/MapState.js
  9. 27
      src/engine/states/RelicSelectionState.js
  10. 31
      src/engine/states/RestState.js
  11. 44
      src/engine/states/ShopState.js
  12. 28
      src/engine/states/VictoryState.js
  13. 7
      src/input/InputManager.js
  14. 129
      src/main.js
  15. 29
      src/ui/render.js

1
src/commands/CommandInvoker.js

@ -27,7 +27,6 @@ export class CommandInvoker {
} }
command.executed = true; command.executed = true;
console.log(`Executed: ${command.getDescription()}`);
} }
return success; return success;

134
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();
}
}

23
src/engine/events.js

@ -191,9 +191,9 @@ export class EventHandler {
// Reset button // Reset button
const resetBtn = this.root.app.querySelector("[data-reset]"); const resetBtn = this.root.app.querySelector("[data-reset]");
if (resetBtn) { if (resetBtn) {
this.on(resetBtn, "click", () => { this.on(resetBtn, "click", async () => {
this.root.clearSave(); this.root.clearSave();
this.root.reset(); await this.root.reset();
}); });
} }
@ -330,19 +330,12 @@ export class EventHandler {
setupEventEvents(event) { setupEventEvents(event) {
this.switchScreen('event'); this.switchScreen('event');
this.root.app.querySelectorAll("[data-choice]").forEach(btn => { // Event choice handlers are managed by InputManager
this.on(btn, "click", () => { // Just set up keyboard shortcuts here
const idx = parseInt(btn.dataset.choice, 10);
event.choices[idx].effect();
this.root.afterNode();
});
});
// Keyboard shortcuts for event choices
for (let i = 1; i <= event.choices.length; i++) { for (let i = 1; i <= event.choices.length; i++) {
this.addKeyHandler(i.toString(), () => { this.addKeyHandler(i.toString(), async () => {
event.choices[i - 1].effect(); event.choices[i - 1].effect();
this.root.afterNode(); await this.root.afterNode();
}, `Event Choice ${i}`); }, `Event Choice ${i}`);
} }
} }
@ -388,7 +381,7 @@ export class EventHandler {
const menuBtn = this.root.app.querySelector("[data-menu]"); const menuBtn = this.root.app.querySelector("[data-menu]");
if (replayBtn) { if (replayBtn) {
this.on(replayBtn, "click", () => this.root.reset()); this.on(replayBtn, "click", async () => await this.root.reset());
} }
if (restartAct2Btn) { if (restartAct2Btn) {
@ -403,7 +396,7 @@ export class EventHandler {
} }
if (menuBtn) { if (menuBtn) {
this.on(menuBtn, "click", () => this.root.reset()); this.on(menuBtn, "click", async () => await this.root.reset());
} }
// Keyboard shortcuts // Keyboard shortcuts

56
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;
}
}

28
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
};
}
}

41
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;
}
}

55
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
}
}

43
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;
}
}

27
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)
};
}
}

31
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
};
}
}

44
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;
}
}

28
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
};
}
}

7
src/input/InputManager.js

@ -3,8 +3,6 @@
* *
* This class consolidates ALL event listeners from the render functions * This class consolidates ALL event listeners from the render functions
* into one place while maintaining exact same functionality. * into one place while maintaining exact same functionality.
*
* Following Nystrom's Input Handling patterns from Game Programming Patterns
*/ */
import { PlayCardCommand } from '../commands/PlayCardCommand.js'; import { PlayCardCommand } from '../commands/PlayCardCommand.js';
@ -220,12 +218,13 @@ export class InputManager {
/** /**
* Handle event choice clicks * Handle event choice clicks
*/ */
handleEventChoice(element, event) { async handleEventChoice(element, event) {
const idx = parseInt(element.dataset.choice, 10); const idx = parseInt(element.dataset.choice, 10);
// Get the current event from the root (this will need to be accessible) // Get the current event from the root (this will need to be accessible)
if (this.root.currentEvent && this.root.currentEvent.choices[idx]) { if (this.root.currentEvent && this.root.currentEvent.choices[idx]) {
this.root.currentEvent.choices[idx].effect(); this.root.currentEvent.choices[idx].effect();
this.root.afterNode(); await this.root.afterNode();
} }
} }

129
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 { renderBattle, renderMap, renderReward, renderRest, renderShop, renderWin, renderLose, renderEvent, renderRelicSelection, renderUpgrade, updateCardSelection, showDamageNumber, renderCodeReviewSelection } from "./ui/render.js";
import { InputManager } from "./input/InputManager.js"; import { InputManager } from "./input/InputManager.js";
import { CommandInvoker } from "./commands/CommandInvoker.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"); const app = document.getElementById("app");
@ -20,6 +29,7 @@ const root = {
enemy: null, enemy: null,
inputManager: null, // Will be initialized later inputManager: null, // Will be initialized later
commandInvoker: new CommandInvoker(), commandInvoker: new CommandInvoker(),
stateMachine: null, // Will be initialized below
currentEvent: null, // For event handling currentEvent: null, // For event handling
currentShopCards: null, // For shop handling currentShopCards: null, // For shop handling
currentShopRelic: null, // For shop relic handling currentShopRelic: null, // For shop relic handling
@ -27,7 +37,14 @@ const root = {
_codeReviewCallback: null, // For code review completion _codeReviewCallback: null, // For code review completion
log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, 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) { play(i) {
const battleCtx = makeBattleContext(this); const battleCtx = makeBattleContext(this);
playCard(battleCtx, i); playCard(battleCtx, i);
@ -44,23 +61,17 @@ const root = {
const node = this.map.nodes.find(n => n.id === nextId); const node = this.map.nodes.find(n => n.id === nextId);
if (!node) return; if (!node) return;
// Use state machine for transitions
if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") {
await this.stateMachine.setState('BATTLE');
this._battleInProgress = true; } else if (node.kind === "rest") {
createBattle(this, node.enemy); await this.stateMachine.setState('REST');
await renderBattle(this); } else if (node.kind === "shop") {
} else { await this.stateMachine.setState('SHOP');
} else if (node.kind === "event") {
this.save(); await this.stateMachine.setState('EVENT');
if (node.kind === "rest") { } else if (node.kind === "start") {
await renderRest(this); await this.stateMachine.setState('MAP');
} else if (node.kind === "shop") {
renderShop(this);
} else if (node.kind === "event") {
renderEvent(this);
} else if (node.kind === "start") {
await renderMap(this);
}
} }
}, },
@ -69,8 +80,8 @@ const root = {
this.completedNodes.push(this.nodeId); this.completedNodes.push(this.nodeId);
} }
const node = this.map.nodes.find(n => n.id === this.nodeId); const node = this.map.nodes.find(n => n.id === this.nodeId);
if (node.kind === "battle" || node.kind === "elite") { if (node.kind === "battle" || node.kind === "elite") {
const choices = pickCards(3); const choices = pickCards(3);
this._pendingChoices = choices; this._pendingChoices = choices;
@ -78,10 +89,11 @@ const root = {
return; return;
} }
if (node.kind === "boss") { 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) { async takeReward(idx) {
@ -92,12 +104,12 @@ const root = {
} }
this._pendingChoices = null; this._pendingChoices = null;
this.save(); this.save();
await renderMap(this); await this.stateMachine.setState('MAP');
}, },
async skipReward() { async skipReward() {
this._pendingChoices = null; this._pendingChoices = null;
this.save(); this.save();
await renderMap(this); await this.stateMachine.setState('MAP');
}, },
async onWin() { async onWin() {
@ -111,9 +123,11 @@ const root = {
this._battleInProgress = false; this._battleInProgress = false;
const node = this.map.nodes.find(n => n.id === this.nodeId); const node = this.map.nodes.find(n => n.id === this.nodeId);
if (node.kind === "boss") { if (node.kind === "boss") {
// Check if there's a next act // Check if there's a next act
const nextAct = this.currentAct === "act1" ? "act2" : null; const nextAct = this.currentAct === "act1" ? "act2" : null;
if (nextAct && MAPS[nextAct]) { if (nextAct && MAPS[nextAct]) {
// Advance to next act // Advance to next act
this.currentAct = nextAct; this.currentAct = nextAct;
@ -128,27 +142,27 @@ const root = {
} }
this.save(); this.save();
await renderMap(this); await this.stateMachine.setState('MAP');
} else { } else {
// Final victory // Final victory
this.save(); // Save progress before clearing on victory this.save(); // Save progress before clearing on victory
this.clearSave(); // Clear save on victory this.clearSave(); // Clear save on victory
await renderWin(this); await this.stateMachine.setState('VICTORY');
} }
} }
else { else {
this.save(); this.save();
this.afterNode(); await this.afterNode();
} }
}, },
async onLose() { async onLose() {
this._battleInProgress = false; this._battleInProgress = false;
this.clearSave(); // Clear save on defeat this.clearSave(); // Clear save on defeat
await renderLose(this); await this.stateMachine.setState('DEFEAT');
}, },
reset() { async reset() {
this.logs = []; this.logs = [];
this.player = makePlayer(); this.player = makePlayer();
initDeck(this.player); initDeck(this.player);
@ -157,13 +171,13 @@ const root = {
this.nodeId = "n1"; this.nodeId = "n1";
this.completedNodes = []; this.completedNodes = [];
renderRelicSelection(this); await this.stateMachine.setState('RELIC_SELECTION');
}, },
async selectStartingRelic(relicId) { async selectStartingRelic(relicId) {
attachRelics(this, [relicId]); attachRelics(this, [relicId]);
this.save(); this.save();
await renderMap(this); await this.stateMachine.setState('MAP');
}, },
save() { save() {
@ -176,6 +190,7 @@ const root = {
completedNodes: this.completedNodes, completedNodes: this.completedNodes,
logs: this.logs.slice(-50), // Keep last 50 logs logs: this.logs.slice(-50), // Keep last 50 logs
battleInProgress: this._battleInProgress || false, battleInProgress: this._battleInProgress || false,
stateMachine: this.stateMachine ? this.stateMachine.getSaveData() : null,
timestamp: Date.now() timestamp: Date.now()
}; };
localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); localStorage.setItem('birthday-spire-save', JSON.stringify(saveData));
@ -300,6 +315,11 @@ const root = {
this.logs = Array.isArray(data.logs) ? data.logs : []; this.logs = Array.isArray(data.logs) ? data.logs : [];
this._battleInProgress = Boolean(data.battleInProgress); this._battleInProgress = Boolean(data.battleInProgress);
// Restore state machine state
if (data.stateMachine && this.stateMachine) {
this.stateMachine.restoreFromSave(data.stateMachine);
}
this.restoreCardEffects(); this.restoreCardEffects();
this.log('Game loaded from save.'); 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) { function pickCards(n) {
const ids = shuffle(CARD_POOL.slice()).slice(0, n); const ids = shuffle(CARD_POOL.slice()).slice(0, n);
return ids.map(id => CARDS[id]); return ids.map(id => CARDS[id]);
@ -394,29 +429,29 @@ async function initializeGame() {
switch (screenParam.toLowerCase()) { switch (screenParam.toLowerCase()) {
case 'victory': case 'victory':
case 'win': case 'win':
await renderWin(root); await root.stateMachine.setState('VICTORY');
return; return;
case 'defeat': case 'defeat':
case 'lose': case 'lose':
await renderLose(root); await root.stateMachine.setState('DEFEAT');
return; return;
case 'map': case 'map':
await renderMap(root); await root.stateMachine.setState('MAP');
return; return;
case 'shop': case 'shop':
renderShop(root); await root.stateMachine.setState('SHOP');
return; return;
case 'rest': case 'rest':
await renderRest(root); await root.stateMachine.setState('REST');
return; return;
case 'event': case 'event':
renderEvent(root); await root.stateMachine.setState('EVENT');
return; return;
case 'battle': case 'battle':
root.go('n2'); // Battle node root.go('n2'); // Battle node (uses state machine internally)
return; return;
case 'upgrade': case 'upgrade':
await renderRest(root); await root.stateMachine.setState('REST');
setTimeout(() => { setTimeout(() => {
const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); const upgradeBtn = root.app.querySelector("[data-act='upgrade']");
if (upgradeBtn) upgradeBtn.click(); if (upgradeBtn) upgradeBtn.click();
@ -424,7 +459,7 @@ async function initializeGame() {
return; return;
case 'relic': case 'relic':
case 'relics': case 'relics':
renderRelicSelection(root); await root.stateMachine.setState('RELIC_SELECTION');
return; return;
default: default:
console.warn(`Unknown screen: ${screenParam}. Loading normal game.`); console.warn(`Unknown screen: ${screenParam}. Loading normal game.`);
@ -541,19 +576,23 @@ async function loadNormalGame() {
} else { } else {
// Battle state inconsistent, go to map // Battle state inconsistent, go to map
root._battleInProgress = false; root._battleInProgress = false;
await renderMap(root); await root.stateMachine.setState('MAP');
} }
} else { } else {
await renderMap(root); await root.stateMachine.setState('MAP');
} }
} else { } else {
root.reset(); await root.reset();
} }
} }
// Initialize InputManager // Initialize InputManager
root.inputManager = new InputManager(root); try {
root.inputManager.initGlobalListeners(); root.inputManager = new InputManager(root);
root.inputManager.initGlobalListeners();
} catch (error) {
console.error('Error initializing InputManager:', error);
}
// Make modules available globally for InputManager // Make modules available globally for InputManager
window.gameModules = { window.gameModules = {
@ -561,5 +600,7 @@ window.gameModules = {
render: { renderMap, renderUpgrade, updateCardSelection, renderCodeReviewSelection } render: { renderMap, renderUpgrade, updateCardSelection, renderCodeReviewSelection }
}; };
initializeGame(); initializeGame().catch(error => {
console.error('Error during game initialization:', error);
});

29
src/ui/render.js

@ -11,7 +11,6 @@ function playSound(soundFile) {
} }
export function showDamageNumber(damage, target, isPlayer = false) { export function showDamageNumber(damage, target, isPlayer = false) {
console.log('this is shown - damage number')
const targetElement = isPlayer ? const targetElement = isPlayer ?
document.querySelector('.player-battle-zone') : document.querySelector('.player-battle-zone') :
document.querySelector('.enemy-battle-zone'); document.querySelector('.enemy-battle-zone');
@ -1125,9 +1124,15 @@ export function renderEvent(root) {
icon: "assets/card-art/apple.png", icon: "assets/card-art/apple.png",
risk: "high", risk: "high",
effect: () => { 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.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", icon: "assets/card-art/heart.png",
risk: "low", risk: "low",
effect: () => { 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"); root.log("Small bite: +8 HP");
} }
}, },
@ -1245,6 +1253,9 @@ export function renderEvent(root) {
]; ];
const event = events[Math.floor(Math.random() * events.length)]; 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 = ` root.app.innerHTML = `
<div class="event-screen"> <div class="event-screen">
@ -1296,13 +1307,7 @@ export function renderEvent(root) {
</div> </div>
`; `;
root.app.querySelectorAll("[data-choice]").forEach(btn => { // Event handlers are managed by InputManager - no need to add them here
btn.addEventListener("click", () => {
const idx = parseInt(btn.dataset.choice, 10);
event.choices[idx].effect();
root.afterNode();
});
});
} }
export async function renderWin(root) { export async function renderWin(root) {
@ -1402,7 +1407,7 @@ export async function renderWin(root) {
</div> </div>
</div> </div>
`; `;
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) { export async function renderCodeReviewSelection(root, cards) {

Loading…
Cancel
Save