Browse Source

refactor

main
Stephanie Gredell 4 months ago
parent
commit
ee94e80b06
  1. 26
      src/commands/Command.js
  2. 54
      src/commands/CommandInvoker.js
  3. 27
      src/commands/EndTurnCommand.js
  4. 28
      src/commands/MapMoveCommand.js
  5. 40
      src/commands/PickRewardCommand.js
  6. 30
      src/commands/PlayCardCommand.js
  7. 42
      src/commands/RestActionCommand.js
  8. 30
      src/commands/RewardPickCommand.js
  9. 5
      src/engine/battle.js
  10. 56
      src/input/InputManager.js
  11. 12
      src/main.js

26
src/commands/Command.js

@ -0,0 +1,26 @@ @@ -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;
}
}

54
src/commands/CommandInvoker.js

@ -0,0 +1,54 @@ @@ -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<string>}
*/
getHistory() {
return this.history.map(cmd => cmd.getDescription());
}
/**
* Clear command history
*/
clear() {
this.history = [];
}
}

27
src/commands/EndTurnCommand.js

@ -0,0 +1,27 @@ @@ -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";
}
}

28
src/commands/MapMoveCommand.js

@ -0,0 +1,28 @@ @@ -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}`;
}
}

40
src/commands/PickRewardCommand.js

@ -0,0 +1,40 @@ @@ -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}`;
}
}

30
src/commands/PlayCardCommand.js

@ -0,0 +1,30 @@ @@ -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}`;
}
}

42
src/commands/RestActionCommand.js

@ -0,0 +1,42 @@ @@ -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}`;
}
}

30
src/commands/RewardPickCommand.js

@ -0,0 +1,30 @@ @@ -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}`;
}
}

5
src/engine/battle.js

@ -210,11 +210,14 @@ function applyDamage(ctx, target, raw, label) { @@ -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' : ''}.`) },

56
src/input/InputManager.js

@ -7,6 +7,12 @@ @@ -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,14 +140,17 @@ export class InputManager { @@ -134,14 +140,17 @@ 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);
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 { @@ -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 { @@ -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 { @@ -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,13 +286,17 @@ export class InputManager { @@ -270,13 +286,17 @@ 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);
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);
}

12
src/main.js

@ -6,6 +6,7 @@ import { makePlayer, initDeck, draw } from "./engine/core.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 = { @@ -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) {

Loading…
Cancel
Save