From ee94e80b065903dfa29396caf0311b22792f9fcf Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Fri, 5 Sep 2025 12:32:16 -0700 Subject: [PATCH] refactor --- src/commands/Command.js | 26 +++++++++++ src/commands/CommandInvoker.js | 54 +++++++++++++++++++++++ src/commands/EndTurnCommand.js | 27 ++++++++++++ src/commands/MapMoveCommand.js | 28 ++++++++++++ src/commands/PickRewardCommand.js | 40 +++++++++++++++++ src/commands/PlayCardCommand.js | 30 +++++++++++++ src/commands/RestActionCommand.js | 42 ++++++++++++++++++ src/commands/RewardPickCommand.js | 30 +++++++++++++ src/engine/battle.js | 5 ++- src/input/InputManager.js | 72 ++++++++++++++++++++----------- src/main.js | 12 +++++- 11 files changed, 337 insertions(+), 29 deletions(-) create mode 100644 src/commands/Command.js create mode 100644 src/commands/CommandInvoker.js create mode 100644 src/commands/EndTurnCommand.js create mode 100644 src/commands/MapMoveCommand.js create mode 100644 src/commands/PickRewardCommand.js create mode 100644 src/commands/PlayCardCommand.js create mode 100644 src/commands/RestActionCommand.js create mode 100644 src/commands/RewardPickCommand.js diff --git a/src/commands/Command.js b/src/commands/Command.js new file mode 100644 index 0000000..9abd33e --- /dev/null +++ b/src/commands/Command.js @@ -0,0 +1,26 @@ +/** + * Base Command class following the Command Pattern + * All game actions should extend this class + */ +export class Command { + constructor() { + this.executed = false; + this.timestamp = Date.now(); + } + + /** + * Execute the command + * @returns {boolean} true if successful, false otherwise + */ + execute() { + throw new Error("Command.execute() must be implemented by subclass"); + } + + /** + * Get a description of this command for logging/debugging + * @returns {string} + */ + getDescription() { + return this.constructor.name; + } +} \ No newline at end of file diff --git a/src/commands/CommandInvoker.js b/src/commands/CommandInvoker.js new file mode 100644 index 0000000..c0c532c --- /dev/null +++ b/src/commands/CommandInvoker.js @@ -0,0 +1,54 @@ +/** + * CommandInvoker manages command execution + * Follows the Command Pattern for centralized action handling + */ +export class CommandInvoker { + constructor() { + this.history = []; + this.maxHistorySize = 50; // Prevent memory bloat + } + + /** + * Execute a command and add it to history + * @param {Command} command - The command to execute + * @returns {boolean} true if successful + */ + execute(command) { + try { + const success = command.execute(); + + if (success) { + // Add command to history for debugging + this.history.push(command); + + // Trim history if it gets too long + if (this.history.length > this.maxHistorySize) { + this.history.shift(); + } + + command.executed = true; + console.log(`Executed: ${command.getDescription()}`); + } + + return success; + } catch (error) { + console.error(`Command execution failed: ${command.getDescription()}`, error); + return false; + } + } + + /** + * Get command history for debugging + * @returns {Array} + */ + getHistory() { + return this.history.map(cmd => cmd.getDescription()); + } + + /** + * Clear command history + */ + clear() { + this.history = []; + } +} \ No newline at end of file diff --git a/src/commands/EndTurnCommand.js b/src/commands/EndTurnCommand.js new file mode 100644 index 0000000..c8090bf --- /dev/null +++ b/src/commands/EndTurnCommand.js @@ -0,0 +1,27 @@ +import { Command } from './Command.js'; + +/** + * Command for ending the player's turn in battle + * Wraps the existing root.end() method + */ +export class EndTurnCommand extends Command { + constructor(gameRoot) { + super(); + this.gameRoot = gameRoot; + } + + execute() { + try { + // Use existing root.end method (which now creates proper battle context) + this.gameRoot.end(); + return true; + } catch (error) { + console.error("EndTurnCommand execution failed:", error); + return false; + } + } + + getDescription() { + return "End Turn"; + } +} \ No newline at end of file diff --git a/src/commands/MapMoveCommand.js b/src/commands/MapMoveCommand.js new file mode 100644 index 0000000..8919261 --- /dev/null +++ b/src/commands/MapMoveCommand.js @@ -0,0 +1,28 @@ +import { Command } from './Command.js'; + +/** + * Command for moving to a node on the map + * Wraps the existing root.go() method + */ +export class MapMoveCommand extends Command { + constructor(gameRoot, nodeId) { + super(); + this.gameRoot = gameRoot; + this.nodeId = nodeId; + } + + execute() { + try { + // Use existing root.go method + this.gameRoot.go(this.nodeId); + return true; + } catch (error) { + console.error("MapMoveCommand execution failed:", error); + return false; + } + } + + getDescription() { + return `Move to Node: ${this.nodeId}`; + } +} \ No newline at end of file diff --git a/src/commands/PickRewardCommand.js b/src/commands/PickRewardCommand.js new file mode 100644 index 0000000..f3aa15b --- /dev/null +++ b/src/commands/PickRewardCommand.js @@ -0,0 +1,40 @@ +import { Command } from './Command.js'; + +/** + * Command for picking a reward card + * Not undoable as it modifies deck permanently + */ +export class PickRewardCommand extends Command { + constructor(gameRoot, rewardIndex) { + super(); + this.gameRoot = gameRoot; + this.rewardIndex = rewardIndex; + } + + execute() { + if (this.gameRoot.screen !== 'reward') { + console.warn("Cannot pick reward - not on reward screen"); + return false; + } + + try { + // Use existing reward selection logic + this.gameRoot.takeReward(this.rewardIndex); + return true; + } catch (error) { + console.error("PickRewardCommand execution failed:", error); + return false; + } + } + + canUndo() { + // Reward picks are not undoable as they modify deck + return false; + } + + getDescription() { + const reward = this.gameRoot.rewards?.[this.rewardIndex]; + const cardName = reward?.name || 'Unknown Card'; + return `Pick Reward: ${cardName}`; + } +} diff --git a/src/commands/PlayCardCommand.js b/src/commands/PlayCardCommand.js new file mode 100644 index 0000000..ee5040f --- /dev/null +++ b/src/commands/PlayCardCommand.js @@ -0,0 +1,30 @@ +import { Command } from './Command.js'; + +/** + * Command for playing a card in battle + * Wraps the existing root.play() method + */ +export class PlayCardCommand extends Command { + constructor(gameRoot, cardIndex) { + super(); + this.gameRoot = gameRoot; + this.cardIndex = cardIndex; + } + + execute() { + try { + // Use existing root.play method (which now creates proper battle context) + this.gameRoot.play(this.cardIndex); + return true; + } catch (error) { + console.error("PlayCardCommand execution failed:", error); + return false; + } + } + + getDescription() { + const card = this.gameRoot.player.hand[this.cardIndex]; + const cardName = card?.name || 'Unknown Card'; + return `Play Card: ${cardName}`; + } +} \ No newline at end of file diff --git a/src/commands/RestActionCommand.js b/src/commands/RestActionCommand.js new file mode 100644 index 0000000..6f85270 --- /dev/null +++ b/src/commands/RestActionCommand.js @@ -0,0 +1,42 @@ +import { Command } from './Command.js'; + +/** + * Command for rest site actions (heal, upgrade) + * Wraps the existing rest action logic + */ +export class RestActionCommand extends Command { + constructor(gameRoot, action) { + super(); + this.gameRoot = gameRoot; + this.action = action; // 'heal' or 'upgrade' + } + + execute() { + try { + if (this.action === 'heal') { + // Heal 20% of max HP (same as current logic) + const heal = Math.floor(this.gameRoot.player.maxHp * 0.2); + this.gameRoot.player.hp = Math.min(this.gameRoot.player.maxHp, this.gameRoot.player.hp + heal); + this.gameRoot.log(`Healed for ${heal} HP.`); + this.gameRoot.afterNode(); + } else if (this.action === 'upgrade') { + // Show upgrade selection (same as current logic) + if (window.gameModules?.render?.renderUpgrade) { + window.gameModules.render.renderUpgrade(this.gameRoot); + } + } else { + console.warn(`Unknown rest action: ${this.action}`); + return false; + } + + return true; + } catch (error) { + console.error("RestActionCommand execution failed:", error); + return false; + } + } + + getDescription() { + return `Rest Action: ${this.action}`; + } +} \ No newline at end of file diff --git a/src/commands/RewardPickCommand.js b/src/commands/RewardPickCommand.js new file mode 100644 index 0000000..740236a --- /dev/null +++ b/src/commands/RewardPickCommand.js @@ -0,0 +1,30 @@ +import { Command } from './Command.js'; + +/** + * Command for picking a reward card + * Wraps the existing root.takeReward() method + */ +export class RewardPickCommand extends Command { + constructor(gameRoot, rewardIndex) { + super(); + this.gameRoot = gameRoot; + this.rewardIndex = rewardIndex; + } + + execute() { + try { + // Use existing root.takeReward method + this.gameRoot.takeReward(this.rewardIndex); + return true; + } catch (error) { + console.error("RewardPickCommand execution failed:", error); + return false; + } + } + + getDescription() { + const reward = this.gameRoot.rewards?.[this.rewardIndex]; + const cardName = reward?.name || 'Unknown Card'; + return `Pick Reward: ${cardName}`; + } +} diff --git a/src/engine/battle.js b/src/engine/battle.js index b10d84e..f39a658 100644 --- a/src/engine/battle.js +++ b/src/engine/battle.js @@ -210,11 +210,14 @@ function applyDamage(ctx, target, raw, label) { export function makeBattleContext(root) { return { player: root.player, - enemy: null, + enemy: root.enemy, discard: root.player.discard, + relicStates: root.relicStates || [], draw: (n) => draw(root.player, n, root), log: (m) => root.log(m), render: () => root.render(), + onWin: () => root.onWin(), + onLose: () => root.onLose(), intentIsAttack: () => root.enemy.intent.type === "attack", deal: (target, amount) => applyDamage(root, target, amount, target === root.enemy ? "You attack" : `${root.enemy.name} hits you`), applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; root.log(`${who === root.player ? 'You are' : root.enemy.name + ' is'} weakened for ${amt} turn${amt > 1 ? 's' : ''}.`) }, diff --git a/src/input/InputManager.js b/src/input/InputManager.js index 19a899e..2dd87c0 100644 --- a/src/input/InputManager.js +++ b/src/input/InputManager.js @@ -7,6 +7,12 @@ * Following Nystrom's Input Handling patterns from Game Programming Patterns */ +import { PlayCardCommand } from '../commands/PlayCardCommand.js'; +import { EndTurnCommand } from '../commands/EndTurnCommand.js'; +import { MapMoveCommand } from '../commands/MapMoveCommand.js'; +import { RewardPickCommand } from '../commands/RewardPickCommand.js'; +import { RestActionCommand } from '../commands/RestActionCommand.js'; + export class InputManager { constructor(gameRoot) { this.root = gameRoot; @@ -134,13 +140,16 @@ export class InputManager { // Play sound this.playSound('played-card.mp3'); - // Use the root.play method which calls playCard internally - this.root.play(index); + // Create and execute PlayCardCommand + const command = new PlayCardCommand(this.root, index); + const success = this.root.commandInvoker.execute(command); - // Clear card selection - this.root.selectedCardIndex = null; - if (window.gameModules?.render?.updateCardSelection) { - window.gameModules.render.updateCardSelection(this.root); + if (success) { + // Clear card selection + this.root.selectedCardIndex = null; + if (window.gameModules?.render?.updateCardSelection) { + window.gameModules.render.updateCardSelection(this.root); + } } } catch (error) { console.error('Error playing card:', error); @@ -152,7 +161,14 @@ export class InputManager { */ handleMapNodeClick(element, event) { if (!element.dataset.node) return; - this.root.go(element.dataset.node); + + try { + // Create and execute MapMoveCommand + const command = new MapMoveCommand(this.root, element.dataset.node); + this.root.commandInvoker.execute(command); + } catch (error) { + console.error('Error moving on map:', error); + } } /** @@ -160,7 +176,14 @@ export class InputManager { */ handleRewardPick(element, event) { const idx = parseInt(element.dataset.pick, 10); - this.root.takeReward(idx); + + try { + // Create and execute RewardPickCommand + const command = new RewardPickCommand(this.root, idx); + this.root.commandInvoker.execute(command); + } catch (error) { + console.error('Error picking reward:', error); + } } /** @@ -249,19 +272,12 @@ export class InputManager { handleRestAction(element, event) { const action = element.dataset.act; - switch (action) { - case 'heal': - const heal = Math.floor(this.root.player.maxHp * 0.2); - this.root.player.hp = Math.min(this.root.player.maxHp, this.root.player.hp + heal); - this.root.log(`Healed for ${heal} HP.`); - this.root.afterNode(); - break; - case 'upgrade': - // This will need to call renderUpgrade - if (window.gameModules?.render?.renderUpgrade) { - window.gameModules.render.renderUpgrade(this.root); - } - break; + try { + // Create and execute RestActionCommand + const command = new RestActionCommand(this.root, action); + this.root.commandInvoker.execute(command); + } catch (error) { + console.error('Error with rest action:', error); } } @@ -270,12 +286,16 @@ export class InputManager { */ handleEndTurn(element, event) { try { - this.root.end(); + // Create and execute EndTurnCommand + const command = new EndTurnCommand(this.root); + const success = this.root.commandInvoker.execute(command); - // Clear card selection - this.root.selectedCardIndex = null; - if (window.gameModules?.render?.updateCardSelection) { - window.gameModules.render.updateCardSelection(this.root); + if (success) { + // Clear card selection + this.root.selectedCardIndex = null; + if (window.gameModules?.render?.updateCardSelection) { + window.gameModules.render.updateCardSelection(this.root); + } } } catch (error) { console.error('Error ending turn:', error); diff --git a/src/main.js b/src/main.js index efb0e46..e313a31 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import { makePlayer, initDeck, draw } from "./engine/core.js"; import { createBattle, startPlayerTurn, playCard, endTurn, makeBattleContext, attachRelics } from "./engine/battle.js"; import { renderBattle, renderMap, renderReward, renderRest, renderShop, renderWin, renderLose, renderEvent, renderRelicSelection, renderUpgrade, updateCardSelection, showDamageNumber } from "./ui/render.js"; import { InputManager } from "./input/InputManager.js"; +import { CommandInvoker } from "./commands/CommandInvoker.js"; const app = document.getElementById("app"); @@ -18,15 +19,22 @@ const root = { completedNodes: [], enemy: null, inputManager: null, // Will be initialized later + commandInvoker: new CommandInvoker(), currentEvent: null, // For event handling currentShopCards: null, // For shop handling currentShopRelic: null, // For shop relic handling log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, async render() { await renderBattle(this); }, - play(i) { playCard(this, i); }, + play(i) { + const battleCtx = makeBattleContext(this); + playCard(battleCtx, i); + }, showDamageNumber: showDamageNumber, - end() { endTurn(this); }, + end() { + const battleCtx = makeBattleContext(this); + endTurn(battleCtx); + }, async go(nextId) {