diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..c5de8f7 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,117 @@ +# Wowngeon Code Architecture + +## Overview +The Wowngeon roguelike game has been refactored into a modular architecture with clear separation of concerns between frontend and backend components. + +## Backend Architecture + +### Core Classes + +#### `Player` (/player.js) +- Manages player state and position +- Methods: `moveTo()`, `isAt()`, `getState()`, `setState()`, `reset()` +- Properties: x, y, hasKey, hasTreasure + +#### `Monster` (/monster.js) +- Manages monster state and AI behavior +- Methods: `moveTowardPlayer()`, `hasCaughtPlayer()`, `getState()`, `setState()` +- AI Features: Smart pathfinding toward player, collision detection + +#### `DungeonGenerator` (/dungeon.js) +- Handles procedural dungeon generation using ROT.js +- Methods: `generate()`, `getRandomRoomCenter()`, `isFloorTile()`, `isWallTile()` +- Features: Room placement, entrance/exit/treasure positioning + +#### `Game` (/game.js) +- Main game logic coordinator +- Uses Player, Monster, and DungeonGenerator classes +- Manages game state, FOV calculation, and entity interactions +- Methods: `movePlayer()`, `moveMonster()`, `getState()`, `updateFOV()` + +### Supporting Files +- `user.js` - User management and game instantiation +- `index.js` - Server entry point and socket handling +- `dbcalls.js` - Database operations +- `rpccalls.js` - Wownero blockchain integration + +## Frontend Architecture + +### Modular JavaScript Components + +#### `ui.js` +- UI utilities and visual helpers +- Functions: `scrollChat()`, `updateBlockHeight()`, `updateFocusIndicator()` +- Manages teletype animations and browser detection + +#### `socketHandlers.js` +- All WebSocket event handlers +- Events: connect, game_start, game_update, game_over, etc. +- Clean separation of network communication logic + +#### `inputHandler.js` +- Keyboard and form input management +- Handles WASD/arrow key movement, chat input, focus management +- Event delegation for game controls + +#### `game.js` (frontend) +- Core game rendering and client-side state management +- ROT.js display integration, FOV rendering, entity drawing +- Maintained original structure for compatibility + +### HTML Structure +- `index.html` - Streamlined main page that loads modular components +- `index_original.html` - Backup of original monolithic structure + +## Key Improvements + +### Backend Benefits +1. **Modularity**: Clear separation of Player, Monster, and Dungeon logic +2. **Maintainability**: Each class has single responsibility +3. **Testability**: Individual components can be unit tested +4. **Extensibility**: Easy to add new entity types or game mechanics + +### Frontend Benefits +1. **Reduced Complexity**: 471-line index.html reduced to ~80 lines +2. **Code Organization**: Related functionality grouped into logical modules +3. **Reusability**: UI and input handlers can be reused/extended +4. **Debugging**: Easier to locate and fix issues in specific modules + +## File Structure +``` +/wowngeon/ +├── Backend Classes +│ ├── player.js # Player entity and state +│ ├── monster.js # Monster AI and behavior +│ ├── dungeon.js # Procedural generation +│ └── game.js # Main game coordinator +├── Backend Support +│ ├── index.js # Server and socket handling +│ ├── user.js # User management +│ └── ...other files +└── Frontend + ├── index.html # Clean modular HTML + └── js/ + ├── game.js # Core game rendering + ├── ui.js # UI utilities + ├── socketHandlers.js # Network communication + ├── inputHandler.js # Input management + └── ...libraries +``` + +## Migration Summary +- ✅ Backend modularized into Player, Monster, DungeonGenerator classes +- ✅ Frontend split into logical modules (UI, Socket, Input handlers) +- ✅ Removed unused files (game_refactored.js, displayManager.js, etc.) +- ✅ Maintained full game functionality and compatibility +- ✅ All game mechanics verified working (movement, FOV, collision, treasure) + +## Testing Verification +- [x] Server starts without errors +- [x] Game initialization works +- [x] Player movement functional +- [x] Monster AI and collision detection operational +- [x] Treasure collection working +- [x] Frontend modules load correctly +- [x] All socket events properly handled + +The refactored codebase is now much more maintainable and follows modern software engineering practices while preserving all original game functionality. diff --git a/dungeon.js b/dungeon.js new file mode 100644 index 0000000..493af29 --- /dev/null +++ b/dungeon.js @@ -0,0 +1,92 @@ +const ROT = require('./rot.js'); + +/** + * Dungeon generation utilities + */ +class DungeonGenerator { + static generate(width, height) { + // Create dungeon using ROT.js Map.Digger + const digger = new ROT.Map.Digger(width, height, { + roomWidth: [3, 9], + roomHeight: [3, 7], + corridorLength: [2, 6], + dugPercentage: 0.2 + }); + + // Initialize the empty map with walls (1) + const map = Array(height).fill().map(() => Array(width).fill(1)); + + // Fill in floors (0) using digger's callback + digger.create((x, y, value) => { + // value: 0 = floor, 1 = wall + map[y][x] = value; + }); + + // Get rooms that were created + const rooms = digger.getRooms(); + + // Place entrance in the first room + const entranceRoom = rooms[0]; + const entranceCenter = entranceRoom.getCenter(); + const entrance = [entranceCenter[0], entranceCenter[1]]; + + // Place exit in the last room + const exitRoom = rooms[rooms.length - 1]; + const exitCenter = exitRoom.getCenter(); + const exit = [exitCenter[0], exitCenter[1]]; + + // Place treasure in a middle room (not first or last) + let treasureRoom; + if (rooms.length > 2) { + treasureRoom = rooms[Math.floor(rooms.length / 2)]; + } else { + // Fallback if we have fewer than 3 rooms + treasureRoom = rooms[0]; + } + const treasureCenter = treasureRoom.getCenter(); + // Place treasure slightly off center to make it more interesting + const treasure = [ + treasureCenter[0] + Math.floor(Math.random() * 3) - 1, + treasureCenter[1] + Math.floor(Math.random() * 3) - 1 + ]; + + // Ensure the treasure is on a floor tile + if (map[treasure[1]][treasure[0]] !== 0) { + treasure[0] = treasureCenter[0]; + treasure[1] = treasureCenter[1]; + } + + return { + map: map, + rooms: rooms, + entrance: entrance, + exit: exit, + treasure: treasure + }; + } + + static getRandomRoomCenter(rooms, excludeFirst = false, excludeLast = false) { + if (!rooms || rooms.length === 0) return null; + + let availableRooms = [...rooms]; + if (excludeFirst && availableRooms.length > 1) { + availableRooms = availableRooms.slice(1); + } + if (excludeLast && availableRooms.length > 1) { + availableRooms = availableRooms.slice(0, -1); + } + + const randomRoom = availableRooms[Math.floor(Math.random() * availableRooms.length)]; + return randomRoom.getCenter(); + } + + static isFloorTile(map, x, y) { + return map[y] && map[y][x] === 0; + } + + static isWallTile(map, x, y) { + return map[y] && map[y][x] === 1; + } +} + +module.exports = DungeonGenerator; diff --git a/game.js b/game.js index 02df8d8..4237c17 100644 --- a/game.js +++ b/game.js @@ -1,66 +1,7 @@ const ROT = require('./rot.js'); - -// Define the missing generateDungeon function -function generateDungeon(width, height) { - // Create dungeon using ROT.js Map.Digger - const digger = new ROT.Map.Digger(width, height, { - roomWidth: [3, 9], - roomHeight: [3, 7], - corridorLength: [2, 6], - dugPercentage: 0.2 - }); - - // Initialize the empty map with walls (1) - const map = Array(height).fill().map(() => Array(width).fill(1)); - - // Fill in floors (0) using digger's callback - digger.create((x, y, value) => { - // value: 0 = floor, 1 = wall - map[y][x] = value; - }); - - // Get rooms that were created - const rooms = digger.getRooms(); - - // Place entrance in the first room - const entranceRoom = rooms[0]; - const entranceCenter = entranceRoom.getCenter(); - const entrance = [entranceCenter[0], entranceCenter[1]]; - - // Place exit in the last room - const exitRoom = rooms[rooms.length - 1]; - const exitCenter = exitRoom.getCenter(); - const exit = [exitCenter[0], exitCenter[1]]; - - // Place treasure in a middle room (not first or last) - let treasureRoom; - if (rooms.length > 2) { - treasureRoom = rooms[Math.floor(rooms.length / 2)]; - } else { - // Fallback if we have fewer than 3 rooms - treasureRoom = rooms[0]; - } - const treasureCenter = treasureRoom.getCenter(); - // Place treasure slightly off center to make it more interesting - const treasure = [ - treasureCenter[0] + Math.floor(Math.random() * 3) - 1, - treasureCenter[1] + Math.floor(Math.random() * 3) - 1 - ]; - - // Ensure the treasure is on a floor tile - if (map[treasure[1]][treasure[0]] !== 0) { - treasure[0] = treasureCenter[0]; - treasure[1] = treasureCenter[1]; - } - - return { - map: map, - rooms: rooms, - entrance: entrance, - exit: exit, - treasure: treasure - }; -} +const Player = require('./player.js'); +const Monster = require('./monster.js'); +const DungeonGenerator = require('./dungeon.js'); class Game { constructor(socketId, mapWidth, mapHeight) { @@ -70,8 +11,8 @@ class Game { this.gameState = 'waiting'; // waiting, active, won, lost this.startBlock = null; this.dungeon = null; - this.player = { x: 0, y: 0, hasKey: false, hasTreasure: false }; - this.monster = { x: 0, y: 0 }; + this.player = new Player(); + this.monster = new Monster(); this.fee = 0; // Amount player paid this.visibleTiles = {}; // Will store visible tiles this.fov = null; // FOV calculator @@ -79,20 +20,17 @@ class Game { } generateDungeon() { - const dungeon = generateDungeon(this.width, this.height); - this.dungeon = dungeon; + this.dungeon = DungeonGenerator.generate(this.width, this.height); - if (dungeon.entrance) { - this.player.x = dungeon.entrance[0]; - this.player.y = dungeon.entrance[1]; + if (this.dungeon.entrance) { + this.player.moveTo(this.dungeon.entrance[0], this.dungeon.entrance[1]); } // Place monster far from entrance - if (dungeon.rooms && dungeon.rooms.length > 2) { - const monsterRoom = dungeon.rooms[Math.floor(dungeon.rooms.length * 0.7)]; + if (this.dungeon.rooms && this.dungeon.rooms.length > 2) { + const monsterRoom = this.dungeon.rooms[Math.floor(this.dungeon.rooms.length * 0.7)]; const center = monsterRoom.getCenter(); - this.monster.x = center[0]; - this.monster.y = center[1]; + this.monster.moveTo(center[0], center[1]); } // Initialize FOV calculator @@ -142,28 +80,27 @@ class Game { this.dungeon.map[newY][newX] !== undefined && this.dungeon.map[newY][newX] === 0) { // 0 is floor - this.player.x = newX; - this.player.y = newY; + this.player.moveTo(newX, newY); console.log(`Player moved to ${newX},${newY} in game for socket ${this.socketId}`); // Update FOV after moving this.updateFOV(); // Check for game events like finding treasure or exit - if (this.dungeon.exit && newX === this.dungeon.exit[0] && newY === this.dungeon.exit[1]) { + if (this.dungeon.exit && this.player.isAt(this.dungeon.exit[0], this.dungeon.exit[1])) { // Handle win condition (e.g., escaped) - this.gameState = 'won'; // Or some other state - return { status: 'moved', event: 'escaped', player: this.player, visibleTiles: this.visibleTiles }; + this.gameState = 'won'; + return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles }; } - if (this.dungeon.treasure && newX === this.dungeon.treasure[0] && newY === this.dungeon.treasure[1] && !this.player.hasTreasure) { + if (this.dungeon.treasure && this.player.isAt(this.dungeon.treasure[0], this.dungeon.treasure[1]) && !this.player.hasTreasure) { this.player.hasTreasure = true; // Remove treasure from dungeon so it won't be sent in future updates this.dungeon.treasure = null; console.log(`Player ${this.socketId} collected treasure! hasTreasure: ${this.player.hasTreasure}`); - return { status: 'moved', event: 'treasure_found', player: this.player, visibleTiles: this.visibleTiles }; + return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles }; } - return { status: 'moved', player: this.player, visibleTiles: this.visibleTiles }; + return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles }; } console.log(`Player move to ${newX},${newY} for socket ${this.socketId} is invalid (wall or out of bounds).`); @@ -171,55 +108,15 @@ class Game { } moveMonster() { - // Simple monster AI to move toward player - const dx = Math.sign(this.player.x - this.monster.x); - const dy = Math.sign(this.player.y - this.monster.y); - - // Try horizontal move - if (dx !== 0) { - if (this.dungeon.map[this.monster.y][this.monster.x + dx] === 0) { - this.monster.x += dx; - return; - } - } - - // Try vertical move - if (dy !== 0) { - if (this.dungeon.map[this.monster.y + dy][this.monster.x] === 0) { - this.monster.y += dy; - return; - } - } - - // Try random direction if direct path is blocked - const dirs = [[0,1],[1,0],[0,-1],[-1,0]]; - const shuffledDirs = ROT.RNG.shuffle(dirs.slice()); - - for (const [dx, dy] of shuffledDirs) { - const nx = this.monster.x + dx; - const ny = this.monster.y + dy; - if (this.dungeon.map[ny] && this.dungeon.map[ny][nx] === 0) { - this.monster.x = nx; - this.monster.y = ny; - break; - } - } + this.monster.moveTowardPlayer(this.player, this.dungeon); } // Update the getState() method to include all entities with relative positions getState() { - // Calculate relative position of all entities from player's perspective - // const playerX = this.player.x; // Not needed if sending absolute visibleTiles - // const playerY = this.player.y; // Not needed if sending absolute visibleTiles - - // The client-side Game.js expects absolute coordinates for visibleTiles. - // So, no conversion to relative coordinates is needed here for visibleTiles. - // Just ensure this.visibleTiles is correctly populated by updateFOV. - const state = { gameState: this.gameState, - player: { ...this.player }, // Send a copy of player object - monster: this.monster ? { ...this.monster } : null, // Send a copy if monster exists + player: this.player.getState(), // Use Player's getState method + monster: this.monster ? this.monster.getState() : null, // Use Monster's getState method visibleTiles: { ...this.visibleTiles }, // Send a copy of visible tiles entrance: this.dungeon ? this.dungeon.entrance : null, exit: this.dungeon ? this.dungeon.exit : null, @@ -235,21 +132,9 @@ class Game { // Check if monster killed player function checkMonsterKill(player, monster) { - // Check for direct overlap - monster on same tile as player - if (monster.x === player.x && monster.y === player.y) { - return true; // Monster killed player - } - - // Check for adjacent tiles (optional - if you want 1-tile proximity kills) - const dx = Math.abs(monster.x - player.x); - const dy = Math.abs(monster.y - player.y); - - // If monster is adjacent (one tile away in any direction) - if ((dx <= 1 && dy === 0) || (dx === 0 && dy <= 1)) { - return true; // Monster killed player - } - - return false; // Player is safe + // Use Monster's hasCaughtPlayer method + const monsterInstance = new Monster(monster.x, monster.y); + return monsterInstance.hasCaughtPlayer(player); } module.exports = Game; diff --git a/html/index.html b/html/index.html index 55aecf5..4b43d16 100644 --- a/html/index.html +++ b/html/index.html @@ -5,30 +5,25 @@ - + - + - + - + - + - - -
@@ -69,402 +64,30 @@
- - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + }); + - + \ No newline at end of file diff --git a/html/index_clean.html b/html/index_clean.html new file mode 100644 index 0000000..40fdb39 --- /dev/null +++ b/html/index_clean.html @@ -0,0 +1,93 @@ + + + + Wowgue - A Wownero Roguelike + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
Block Height: Loading...
+
Status: Connecting...
+
+ + +
+ + +
+
+
    +
    +
    + + +
    +
    + + +
    +
    + + + +
    + + + + + + + + + + + diff --git a/html/index_original.html b/html/index_original.html new file mode 100644 index 0000000..55aecf5 --- /dev/null +++ b/html/index_original.html @@ -0,0 +1,470 @@ + + + + Wowgue - A Wownero Roguelike + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    +
    Block Height: Loading...
    +
    Status: Connecting...
    +
    + + +
    + + +
    +
    +
      +
      +
      + + +
      +
      + + +
      +
      + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/html/js/displayManager.js b/html/js/displayManager.js deleted file mode 100644 index bffac91..0000000 --- a/html/js/displayManager.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * DisplayManager - Handles all ROT.js display operations - */ -var DisplayManager = { - _display: null, - _screenWidth: 0, - _screenHeight: 0, - _displayReady: false, - _isTileMode: true, - - // Visual representation constants - symbols: { - player: "@", - wall: "#", - floor: "'", - monster: "~", - exit: ">", - entrance: "<", - treasure: "$", - border: "+" - }, - - init: function(screenWidth, screenHeight, containerId) { - console.log("DisplayManager.init called"); - this._screenWidth = screenWidth; - this._screenHeight = screenHeight; - - var displayOptions = { - width: this._screenWidth, - height: this._screenHeight, - forceSquareRatio: true, - layout: "tile", - bg: window.options.bg, - fg: window.options.fg || "#FFF", - tileWidth: window.options.tileWidth, - tileHeight: window.options.tileHeight, - tileSet: window.options.tileSet, - tileMap: window.options.tileMap, - tileColorize: false - }; - - try { - var gameDisplayContainer = document.getElementById(containerId); - if (!gameDisplayContainer) { - console.error("Game display container not found:", containerId); - return false; - } - - this._display = new ROT.Display(displayOptions); - gameDisplayContainer.innerHTML = ''; - gameDisplayContainer.appendChild(this._display.getContainer()); - - console.log("ROT.Display initialized successfully"); - this._displayReady = true; - return true; - } catch (e) { - console.error("Error initializing ROT.Display:", e); - this._displayReady = false; - return false; - } - }, - - isReady: function() { - return this._display && this._displayReady; - }, - - clear: function() { - if (!this.isReady()) return; - - const display = this._display; - display._data = {}; - display._dirty = {}; - - var canvas = display.getContainer(); - if (canvas && typeof canvas.getContext === 'function') { - var ctx = canvas.getContext("2d"); - if (ctx) { - ctx.fillStyle = "#000000"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - } - }, - - draw: function(x, y, char, fg, bg) { - if (!this.isReady()) return; - this._display.draw(x, y, char, fg, bg); - }, - - drawText: function(x, y, text, fg) { - if (!this.isReady()) return; - if (fg) { - this._display.drawText(x, y, `%c{${fg}}${text}`); - } else { - this._display.drawText(x, y, text); - } - }, - - drawCenteredText: function(y, text, fg) { - if (!this.isReady()) return y; - const x = Math.floor((this._screenWidth - text.length) / 2); - this.drawText(x, y, text, fg); - return y + 1; - }, - - drawBorder: function(fg) { - if (!this.isReady()) return; - const borderChar = this.symbols.border; - const color = fg || "#FFF"; - - for (let x = 0; x < this._screenWidth; x++) { - for (let y = 0; y < this._screenHeight; y++) { - if (x === 0 || y === 0 || x === this._screenWidth-1 || y === this._screenHeight-1) { - this.draw(x, y, borderChar, color, null); - } - } - } - }, - - getScreenWidth: function() { - return this._screenWidth; - }, - - getScreenHeight: function() { - return this._screenHeight; - } -}; diff --git a/html/js/gameState.js b/html/js/gameState.js deleted file mode 100644 index 91db60b..0000000 --- a/html/js/gameState.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * GameState - Manages game state and data - */ -var GameState = { - player: null, - monster: null, - entrance: null, - exit: null, - treasure: null, - visibleTiles: {}, - exploredTiles: {}, - gameActive: false, - map: {}, - items: {}, - - init: function() { - this.reset(); - }, - - reset: function() { - this.player = null; - this.monster = null; - this.entrance = null; - this.exit = null; - this.treasure = null; - this.visibleTiles = {}; - this.exploredTiles = {}; - this.gameActive = false; - this.map = {}; - this.items = {}; - }, - - startGame: function(data) { - console.log("Starting game with data:", data); - - if (!data) { - console.error("No game data received!"); - return false; - } - - try { - this.gameActive = true; - - // Set player position - this.player = data.player || { x: 12, y: 12 }; - this.monster = data.monster; - this.entrance = data.entrance; - this.exit = data.exit; - this.treasure = data.treasure; - - // Set visible tiles - if (data.visibleTiles && typeof data.visibleTiles === 'object') { - this.visibleTiles = data.visibleTiles; - console.log("Visible tiles:", Object.keys(this.visibleTiles).length, "rows"); - } else { - // Create a basic visible area around player - console.log("No visible tiles provided, creating basic view"); - this.visibleTiles = {}; - const px = this.player.x; - const py = this.player.y; - - for (let y = py-2; y <= py+2; y++) { - if (!this.visibleTiles[y]) this.visibleTiles[y] = {}; - for (let x = px-2; x <= px+2; x++) { - this.visibleTiles[y][x] = 0; // Floor tiles - } - } - } - - // Update explored tiles - this.updateExploredTiles(); - - return true; - } catch (err) { - console.error("Error starting game:", err); - return false; - } - }, - - updateGameState: function(data) { - console.log("🎲 Updating game state with:", data); - - if (!data) { - console.error("No update data received!"); - return; - } - - try { - // Update player position - if (data.player) { - console.log("Player position updated:", data.player); - this.player = data.player; - } - - // Update monster position - if (data.monster !== undefined) { - this.monster = data.monster; - } - - // Update visible tiles - if (data.visibleTiles && Object.keys(data.visibleTiles).length > 0) { - console.log("Visible tiles updated:", Object.keys(data.visibleTiles).length, "rows"); - this.visibleTiles = data.visibleTiles; - this.updateExploredTiles(); - } - - return true; - } catch (err) { - console.error("Error updating game state:", err); - return false; - } - }, - - updateExploredTiles: function() { - // Add all currently visible tiles to explored tiles - for (var y in this.visibleTiles) { - if (!this.exploredTiles[y]) this.exploredTiles[y] = {}; - - for (var x in this.visibleTiles[y]) { - this.exploredTiles[y][x] = this.visibleTiles[y][x]; - } - } - }, - - setGameActive: function(active) { - this.gameActive = active; - }, - - isGameActive: function() { - return this.gameActive; - }, - - getGameData: function() { - return { - player: this.player, - monster: this.monster, - entrance: this.entrance, - exit: this.exit, - treasure: this.treasure, - visibleTiles: this.visibleTiles, - exploredTiles: this.exploredTiles, - gameActive: this.gameActive, - map: this.map, - items: this.items - }; - } -}; diff --git a/html/js/game_refactored.js b/html/js/game_refactored.js deleted file mode 100644 index 495e6d4..0000000 --- a/html/js/game_refactored.js +++ /dev/null @@ -1,139 +0,0 @@ -// This provides a mock socket when the real one isn't available -if (typeof window !== 'undefined' && !window.socket) { - window.socket = { - emit: function(event, data) { - console.log("[Mock Socket] Would emit:", event, data); - } - }; -} - -/** - * Main Game object - coordinates all the managers - */ -var Game = { - // Managers - displayManager: DisplayManager, - screenManager: ScreenManager, - inputHandler: InputHandler, - gameState: GameState, - - // Config - _screenWidth: options.width, - _screenHeight: options.height, - - init: function() { - console.log("Game.init called"); - - // Initialize all managers - if (!this.displayManager.init(this._screenWidth, this._screenHeight, "game-display")) { - console.error("Failed to initialize display manager"); - return false; - } - - this.screenManager.init(this.displayManager); - this.gameState.init(); - this.inputHandler.init(); - - // Set up input handler events - const self = this; - this.inputHandler.on('movement', function(direction) { - console.log("Movement input received:", direction); - }); - - // Draw welcome screen - this.screenManager.drawWelcomeScreen(); - - console.log("Game initialization complete"); - return true; - }, - - startGame: function(data) { - console.log("🎮 Game start received with data:", data); - - if (!this.gameState.startGame(data)) { - console.error("Failed to start game"); - return false; - } - - // Activate input handling - this.inputHandler.setGameActive(true); - this.gameState.setGameActive(true); - - // Draw the game screen - this.screenManager.drawGameScreen(this.gameState.getGameData()); - - return true; - }, - - updateGameState: function(data) { - if (!this.gameState.updateGameState(data)) { - console.error("Failed to update game state"); - return; - } - - // Redraw the screen if game is active - if (this.gameState.isGameActive()) { - this.screenManager.drawGameScreen(this.gameState.getGameData()); - } - }, - - showWinScreen: function(hasTreasure) { - this.inputHandler.setGameActive(false); - this.gameState.setGameActive(false); - this.screenManager.drawWinScreen(hasTreasure); - }, - - showLoseScreen: function(reason) { - this.inputHandler.setGameActive(false); - this.gameState.setGameActive(false); - this.screenManager.drawLoseScreen(reason); - }, - - showWaitingScreen: function() { - this.inputHandler.setGameActive(false); - this.gameState.setGameActive(false); - this.screenManager.drawWaitingScreen(); - }, - - showWelcomeScreen: function() { - this.inputHandler.setGameActive(false); - this.gameState.setGameActive(false); - this.screenManager.drawWelcomeScreen(); - }, - - clearDisplay: function() { - this.displayManager.clear(); - } -}; - -// Socket event handlers -if (typeof window !== 'undefined' && window.socket) { - socket.on('game_start', function(data) { - console.log("🎮 Game start received with data:", data); - Game.startGame(data); - }); - - socket.on('game_update', function(data) { - Game.updateGameState(data); - }); - - socket.on('waiting_status', function(data) { - console.log("Waiting status received:", data); - Game.showWaitingScreen(); - }); - - socket.on('queue_cancelled', function() { - console.log("Queue cancelled, returning to welcome screen"); - Game.showWelcomeScreen(); - }); - - socket.on('game_over', function(data) { - console.log("Game over received:", data); - - if (data.status === 'won') { - Game.showWinScreen(data.hasTreasure); - } else { - Game.showLoseScreen(data.reason); - } - }); -} diff --git a/html/js/inputHandler.js b/html/js/inputHandler.js index 9de4211..1e49aef 100644 --- a/html/js/inputHandler.js +++ b/html/js/inputHandler.js @@ -1,85 +1,102 @@ /** - * InputHandler - Manages keyboard input and translates to game actions + * Input handlers for the Wowngeon game */ -var InputHandler = { - gameActive: false, - eventCallbacks: {}, - +const InputHandler = { init: function() { - console.log("Initializing keyboard handler"); - - const self = this; - window.addEventListener('keydown', function(e) { - self.handleKeyDown(e); + this.setupChatForm(); + this.setupFocusHandlers(); + this.setupKeyboardControls(); + this.setupModeToggle(); + }, + + setupChatForm: function() { + $('#chatForm').submit(function(e) { + e.preventDefault(); + var msg = $('#chatInput').val().trim(); + + // Don't send empty messages + if (!msg) return false; + + console.log("📝 SENDING CHAT:", msg); + socket.emit('chat message', msg); + $('#chatInput').val(''); + + // If the message is "enter", show the waiting screen + if (msg.toLowerCase() === 'enter') { + console.log("🔑 'ENTER' COMMAND DETECTED - expecting game_start response"); + + // Show waiting screen + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { + Game.drawWaitingScreen(); + } + } + + return false; }); - - console.log("Keyboard handler initialized"); + + // Set initial focus to chat input + $('#chatInput').focus(); + UI.updateFocusIndicator(); }, - setGameActive: function(active) { - this.gameActive = active; + setupFocusHandlers: function() { + // Add focus/blur listeners for visual indicator + $('#chatInput').on('focus', UI.updateFocusIndicator).on('blur', UI.updateFocusIndicator); + $('#game-display').on('focus', UI.updateFocusIndicator).on('blur', UI.updateFocusIndicator); + + // Make game display focusable + $('#game-display').attr('tabindex', '-1'); }, - on: function(eventName, callback) { - if (!this.eventCallbacks[eventName]) { - this.eventCallbacks[eventName] = []; - } - this.eventCallbacks[eventName].push(callback); + setupKeyboardControls: function() { + $(document).on('keydown', function(e) { + if (document.activeElement === $('#game-display')[0]) { + if (Game && Game._gameActive) { + let dx = 0; + let dy = 0; + let moved = false; + + switch(e.key) { + case 'w': case 'ArrowUp': dy = -1; moved = true; break; + case 's': case 'ArrowDown': dy = 1; moved = true; break; + case 'a': case 'ArrowLeft': dx = -1; moved = true; break; + case 'd': case 'ArrowRight': dx = 1; moved = true; break; + } + + if (moved) { + console.log(`Attempting to move: dx=${dx}, dy=${dy}. Key: ${e.key}`); + e.preventDefault(); // Prevent page scrolling + socket.emit('player_move', { dx: dx, dy: dy }); + console.log(`Emitted player_move: dx=${dx}, dy=${dy}`); + } + } else { + console.log("Game display has focus, but Game is not active."); + } + } else if (e.key === 'Enter' && document.activeElement !== $('#chatInput')[0]) { + // If Enter is pressed and chat is not focused, focus chat + $('#chatInput').focus(); + UI.updateFocusIndicator(); + } + }); }, - emit: function(eventName, data) { - if (this.eventCallbacks[eventName]) { - this.eventCallbacks[eventName].forEach(function(callback) { - callback(data); + setupModeToggle: function() { + var toggleButton = document.getElementById('toggle-mode'); + if (toggleButton) { + toggleButton.addEventListener('click', function() { + try { + console.log("Display mode switching is no longer needed - modes are equivalent"); + toggleButton.textContent = 'Mode switching disabled'; + toggleButton.disabled = true; + } catch (e) { + console.error("Error switching display mode:", e); + } }); } - }, - - handleKeyDown: function(e) { - if (!this.gameActive) { - return; - } - - let direction = null; - - switch (e.key) { - case 'ArrowUp': - case 'w': - case 'W': - direction = 'up'; - break; - - case 'ArrowDown': - case 's': - case 'S': - direction = 'down'; - break; - - case 'ArrowLeft': - case 'a': - case 'A': - direction = 'left'; - break; - - case 'ArrowRight': - case 'd': - case 'D': - direction = 'right'; - break; - - default: - return; - } - - e.preventDefault(); - console.log(`Key pressed: ${e.key}, sending direction: ${direction}`); - - // Emit movement event - this.emit('movement', direction); - - // Send to server via socket - if (window.socket && direction) { - window.socket.emit('move', direction); - } } }; + +// Initialize input handlers when DOM is ready +$(function() { + InputHandler.init(); +}); diff --git a/html/js/screenManager.js b/html/js/screenManager.js deleted file mode 100644 index 8a6330b..0000000 --- a/html/js/screenManager.js +++ /dev/null @@ -1,195 +0,0 @@ -/** - * ScreenManager - Handles drawing different game screens - */ -var ScreenManager = { - displayManager: null, - defaultFg: "#FFF", - - init: function(displayManager) { - this.displayManager = displayManager; - }, - - drawWelcomeScreen: function() { - if (!this.displayManager.isReady()) return; - - console.log("Drawing welcome screen"); - this.displayManager.clear(); - this.displayManager.drawBorder(this.defaultFg); - - // Draw content with centered text - let y = 3; - y = this.displayManager.drawCenteredText(y, "WOWGUE", this.defaultFg); - y += 4; - - // Draw instructions - y = this.displayManager.drawCenteredText(y, "Type enter to start!", this.defaultFg); - y = this.displayManager.drawCenteredText(y, "Or type help...", this.defaultFg); - y += 2; - - // Draw legend - const centerX = Math.floor(this.displayManager.getScreenWidth() / 2) - 11; - const symbols = this.displayManager.symbols; - - this.displayManager.draw(centerX, y, symbols.player, this.defaultFg, null); - this.displayManager.drawText(centerX + 2, y, "- This is you", this.defaultFg); - y++; - - this.displayManager.draw(centerX, y, symbols.exit, this.defaultFg, null); - this.displayManager.drawText(centerX + 2, y, "- Escape the dungeon", this.defaultFg); - y++; - - this.displayManager.draw(centerX, y, symbols.monster, this.defaultFg, null); - this.displayManager.drawText(centerX + 2, y, "- Avoid the monster", this.defaultFg); - y++; - - this.displayManager.draw(centerX, y, symbols.treasure, this.defaultFg, null); - this.displayManager.drawText(centerX + 2, y, "- Secure the bag", this.defaultFg); - y++; - - // Draw footer - y += 1; - this.displayManager.drawCenteredText(17, "Type enter in chat =", this.defaultFg); - }, - - drawWinScreen: function(hasTreasure) { - if (!this.displayManager.isReady()) return; - - console.log("Drawing win screen, hasTreasure:", hasTreasure); - this.displayManager.clear(); - this.displayManager.drawBorder(this.defaultFg); - - let y = 5; - y = this.displayManager.drawCenteredText(y, "YOU ESCAPED!", this.defaultFg); - y += 1; - - const subtitle = hasTreasure ? "WITH THE TREASURE!" : "ALIVE!"; - y = this.displayManager.drawCenteredText(y, subtitle, this.defaultFg); - y += 2; - - if (hasTreasure) { - y = this.displayManager.drawCenteredText(y, "You won extra Wownero!", this.defaultFg); - y += 1; - } - - y += 1; - this.displayManager.drawCenteredText(y, "Type ENTER to retry", this.defaultFg); - }, - - drawLoseScreen: function(reason) { - if (!this.displayManager.isReady()) return; - - console.log("Drawing lose screen, reason:", reason); - this.displayManager.clear(); - this.displayManager.drawBorder(this.defaultFg); - - let y = 5; - y = this.displayManager.drawCenteredText(y, "YOU DIED!", this.defaultFg); - y += 1; - - let subtitle = ""; - if (reason === 'monster') { - subtitle = "KILLED BY THE MONSTER"; - } else if (reason === 'timeout') { - subtitle = "YOU RAN OUT OF TIME"; - } - - if (subtitle) { - y = this.displayManager.drawCenteredText(y, subtitle, this.defaultFg); - y += 1; - } - - y += 2; - this.displayManager.drawCenteredText(y, "Type ENTER to retry", this.defaultFg); - }, - - drawWaitingScreen: function() { - if (!this.displayManager.isReady()) return; - - console.log("Drawing waiting screen"); - this.displayManager.clear(); - this.displayManager.drawBorder(this.defaultFg); - - const centerY = Math.floor(this.displayManager.getScreenHeight() / 2); - this.displayManager.drawCenteredText(centerY, "Waiting for next block...", this.defaultFg); - }, - - drawGameScreen: function(gameState) { - if (!this.displayManager.isReady()) return; - - console.log("Drawing game screen..."); - this.displayManager.clear(); - - if (!gameState.player) { - console.error("No player data in game state!"); - return; - } - - const centerX = Math.floor(this.displayManager.getScreenWidth() / 2); - const centerY = Math.floor(this.displayManager.getScreenHeight() / 2); - const symbols = this.displayManager.symbols; - const playerX = gameState.player.x; - const playerY = gameState.player.y; - - // Draw visible tiles centered on player (absolute to relative coordinate conversion) - for (let worldY in gameState.visibleTiles) { - worldY = parseInt(worldY); - for (let worldX in gameState.visibleTiles[worldY]) { - worldX = parseInt(worldX); - - // Convert world coordinates to screen coordinates relative to player - const screenX = centerX + (worldX - playerX); - const screenY = centerY + (worldY - playerY); - - if (screenX >= 0 && screenY >= 0 && - screenX < this.displayManager.getScreenWidth() && - screenY < this.displayManager.getScreenHeight()) { - - const tileType = gameState.visibleTiles[worldY][worldX]; - - if (tileType === 1) { // Wall - this.displayManager.draw(screenX, screenY, symbols.wall, this.defaultFg, null); - } else { // Floor - this.displayManager.draw(screenX, screenY, symbols.floor, this.defaultFg, null); - } - } - } - } - - // Draw entities with stacking (now with correct coordinate conversion) - this._drawEntityStacked(centerX, centerY, playerX, playerY, gameState.entrance, symbols.entrance, gameState.visibleTiles); - this._drawEntityStacked(centerX, centerY, playerX, playerY, gameState.exit, symbols.exit, gameState.visibleTiles); - this._drawEntityStacked(centerX, centerY, playerX, playerY, gameState.treasure, symbols.treasure, gameState.visibleTiles); - this._drawEntityStacked(centerX, centerY, playerX, playerY, gameState.monster, symbols.monster, gameState.visibleTiles); - - // Always draw player at center - const baseTile = gameState.visibleTiles[playerY] && gameState.visibleTiles[playerY][playerX] === 1 ? - symbols.wall : symbols.floor; - this.displayManager.draw(centerX, centerY, [baseTile, symbols.player], - [this.defaultFg, this.defaultFg], [null, null]); - }, - - _drawEntityStacked: function(centerX, centerY, playerX, playerY, entity, symbol, visibleTiles) { - if (!entity) return; - - const entityX = Array.isArray(entity) ? entity[0] : entity.x; - const entityY = Array.isArray(entity) ? entity[1] : entity.y; - - // Convert world coordinates to screen coordinates relative to player - const screenX = centerX + (entityX - playerX); - const screenY = centerY + (entityY - playerY); - - if (screenX >= 0 && screenY >= 0 && - screenX < this.displayManager.getScreenWidth() && - screenY < this.displayManager.getScreenHeight()) { - - // Check if entity is visible - if (visibleTiles[entityY] && visibleTiles[entityY][entityX] !== undefined) { - const baseTile = visibleTiles[entityY][entityX] === 1 ? - this.displayManager.symbols.wall : this.displayManager.symbols.floor; - - this.displayManager.draw(screenX, screenY, [baseTile, symbol], - [this.defaultFg, this.defaultFg], [null, null]); - } - } - } -}; diff --git a/html/js/socketHandlers.js b/html/js/socketHandlers.js new file mode 100644 index 0000000..3507595 --- /dev/null +++ b/html/js/socketHandlers.js @@ -0,0 +1,172 @@ +/** + * Socket event handlers for the Wowngeon game + */ +const SocketHandlers = { + init: function() { + if (!window.socket) { + console.error("Socket not available!"); + return; + } + + this.registerEventHandlers(); + }, + + registerEventHandlers: function() { + // Connection handlers + socket.on('connect', this.onConnect); + socket.on('welcome', this.onWelcome); + socket.on('message', this.onMessage); + + // Game state handlers + socket.on('waiting_status', this.onWaitingStatus); + socket.on('game_start', this.onGameStart); + socket.on('game_update', this.onGameUpdate); + socket.on('game_over', this.onGameOver); + socket.on('queue_cancelled', this.onQueueCancelled); + + // Block height handler + socket.on('blockheight', this.onBlockHeight); + }, + + onConnect: function() { + console.log("Connected with socket ID:", socket.id); + + // Tell server our client ID + socket.emit('register_client', { + clientId: socket.id, + userAgent: navigator.userAgent + }); + }, + + onWelcome: function(msg) { + $('#messages').append($('
    • ').text("Connected to server!")); + UI.scrollChat(); + }, + + onMessage: function(msg) { + $('#messages').append($('
    • ').text(msg)); + UI.scrollChat(); + }, + + onWaitingStatus: function(data) { + console.log("🕒 WAITING STATUS received:", data); + + if (data.status === 'waiting') { + $('#messages').append($('
    • ').text(data.message)); + + // Show waiting screen + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { + Game.drawWaitingScreen(); + } + } + UI.scrollChat(); + }, + + onGameStart: function(data) { + console.log("🎮 Game start received with data:", data); + $('#messages').append($('
    • ').text("Starting game...")); + + if (typeof Game !== 'undefined' && Game.stopWaitingScreen) { + Game.stopWaitingScreen(); + } + + if (!data) { + $('#messages').append($('
    • ').text("Error: No game data received from server.")); + if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); + return; + } + + try { + // Use the original game.js structure + var success = Game.startGame(data.player, data.map, data.monster, data.items, data.visibleTiles); + console.log("Game start result:", success ? "SUCCESS" : "FAILED"); + + if (!success) { + $('#messages').append($('
    • ').text("Game start failed. Check console for details.")); + if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); + } else { + // Draw the initial game screen - the game draws itself after startGame() + console.log("Game started successfully, initial screen should be drawn"); + + // Shift focus to the game display area after successful game start + $('#game-display').focus(); + UI.updateFocusIndicator(); + } + + } catch (err) { + console.error("Error starting game:", err); + $('#messages').append($('
    • ').text("Error: " + err.message)); + } + }, + + onGameUpdate: function(data) { + if (typeof Game !== 'undefined' && Game._gameActive) { + Game.updateGameState(data); + } + }, + + onGameOver: function(data) { + console.log("🎮 GAME OVER received:", data); + + // Update game state based on outcome + if (typeof Game !== 'undefined') { + if (data.status === 'won') { + // Show win message + $('#messages').append($('
    • ').text( + data.hasTreasure ? + "YOU ESCAPED WITH THE TREASURE! YOU WON!" : + "You escaped the dungeon alive!" + )); + + // Draw win screen + Game.drawWinScreen(data.hasTreasure); + } else { + // Show loss message + const reason = data.reason || "unknown"; + let message = "You died in the dungeon!"; + + if (reason === 'monster') { + message = "You were killed by the monster!"; + } else if (reason === 'timeout') { + message = "You didn't escape before the next block was found!"; + } + + $('#messages').append($('
    • ').text(message)); + + // Draw lose screen + if (Game && Game.drawLoseScreen) { + Game.drawLoseScreen(reason); + } + } + + // Set game as inactive to prevent further movement + if (Game) { + Game._gameActive = false; + } + } + + UI.scrollChat(); + }, + + onQueueCancelled: function() { + console.log("Queue cancelled, returning to welcome screen"); + + if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) { + Game._drawWelcomeScreen(); + } else { + console.error("Game or _drawWelcomeScreen not available"); + } + }, + + onBlockHeight: function(height) { + console.log(`📈 BLOCK HEIGHT: ${height}`); + UI.updateBlockHeight(height); + $('#statusValue').text('Connected'); + $('#statusValue').css('color', '#0f0'); + } +}; + +// Initialize when DOM is ready +$(function() { + SocketHandlers.init(); +}); diff --git a/html/js/ui.js b/html/js/ui.js new file mode 100644 index 0000000..43a6cc8 --- /dev/null +++ b/html/js/ui.js @@ -0,0 +1,112 @@ +/** + * UI utilities and helpers for the Wowngeon game + */ +const UI = { + // Scroll chat to bottom + scrollChat: function() { + var messageBox = $('#messageBox'); + messageBox.scrollTop(messageBox.prop("scrollHeight")); + }, + + // Update block height display + updateBlockHeight: function(height) { + $('#heightValue').text(height); + }, + + // Update visual focus indicator + updateFocusIndicator: function() { + var gameDisplay = $('#game-display'); + var chatInput = $('#chatInput'); + + if (document.activeElement === gameDisplay[0]) { + gameDisplay.css('outline', '2px solid yellow'); + chatInput.css('outline', 'none'); + console.log("Focus: Game Display"); + } else if (document.activeElement === chatInput[0]) { + chatInput.css('outline', '2px solid cyan'); + gameDisplay.css('outline', 'none'); + console.log("Focus: Chat Input"); + } else { + gameDisplay.css('outline', 'none'); + chatInput.css('outline', 'none'); + console.log("Focus: None or Other"); + } + }, + + // Initialize teletype animation + initTeletype: function() { + $('#banner').teletype({ + text: ["Welcome traveler...", "You have arrived at...", "The Wowngeon"], + typeDelay: 100, + backDelay: 50, + pause: 1500 + }); + + // After animations are complete, set static text + setTimeout(function() { + $('#banner, #bannercursor').fadeOut(500, function() { + $('#staticbanner').text("THE WOWNGEON"); + }); + }, 14000); + }, + + // Initialize browser detection + initBrowserDetection: function() { + setTimeout(function() { + var browserInfo = document.getElementById('browser-info'); + if (!browserInfo) return; + + browserInfo.style.display = 'block'; + + // Detect browser + var browserName = "Unknown"; + if (navigator.userAgent.indexOf("Firefox") !== -1) { + browserName = "Firefox"; + } else if (navigator.userAgent.indexOf("Chrome") !== -1) { + browserName = "Chrome/Chromium"; + } else if (navigator.userAgent.indexOf("Safari") !== -1) { + browserName = "Safari"; + } else if (navigator.userAgent.indexOf("Edge") !== -1) { + browserName = "Edge"; + } + + document.getElementById('browser-name').textContent = browserName; + + // Check display status + var rotdis = document.querySelector('.rotdis'); + var canvas = rotdis ? rotdis.querySelector('canvas') : null; + + if (canvas) { + document.getElementById('display-status').textContent = + "OK (" + canvas.width + "x" + canvas.height + ")"; + document.getElementById('display-status').style.color = "#0f0"; + } else { + document.getElementById('display-status').textContent = "NOT FOUND"; + document.getElementById('display-status').style.color = "#f00"; + } + + // Hide after 10 seconds + setTimeout(function() { + browserInfo.style.display = 'none'; + }, 10000); + }, 2000); + }, + + // Set test values if no server response + setTestValues: function() { + setTimeout(function() { + if ($('#heightValue').text() === 'Loading...') { + UI.updateBlockHeight('---'); + $('#statusValue').text('Waiting...'); + $('#statusValue').css('color', '#ff0'); + } + }, 5000); + } +}; + +// Initialize UI components when DOM is ready +$(function() { + UI.initTeletype(); + UI.initBrowserDetection(); + UI.setTestValues(); +}); diff --git a/monster.js b/monster.js new file mode 100644 index 0000000..0d048c7 --- /dev/null +++ b/monster.js @@ -0,0 +1,99 @@ +const ROT = require('./rot.js'); + +/** + * Monster class - Manages monster state and AI + */ +class Monster { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + + // Move monster to new position + moveTo(x, y) { + this.x = x; + this.y = y; + } + + // Check if monster is at given coordinates + isAt(x, y) { + return this.x === x && this.y === y; + } + + // Simple AI to move toward player + moveTowardPlayer(player, dungeon) { + if (!player || !dungeon || !dungeon.map) return; + + const dx = Math.sign(player.x - this.x); + const dy = Math.sign(player.y - this.y); + + // Try horizontal move first + if (dx !== 0) { + const newX = this.x + dx; + if (dungeon.map[this.y] && dungeon.map[this.y][newX] === 0) { + this.x = newX; + return; + } + } + + // Try vertical move + if (dy !== 0) { + const newY = this.y + dy; + if (dungeon.map[newY] && dungeon.map[newY][this.x] === 0) { + this.y = newY; + return; + } + } + + // Try random direction if direct path is blocked + const dirs = [[0,1],[1,0],[0,-1],[-1,0]]; + const shuffledDirs = ROT.RNG.shuffle(dirs.slice()); + + for (const [dx, dy] of shuffledDirs) { + const nx = this.x + dx; + const ny = this.y + dy; + if (dungeon.map[ny] && dungeon.map[ny][nx] === 0) { + this.x = nx; + this.y = ny; + break; + } + } + } + + // Check if monster has caught the player + hasCaughtPlayer(player) { + if (!player) return false; + + // Check for direct overlap - monster on same tile as player + if (this.x === player.x && this.y === player.y) { + return true; + } + + // Check for adjacent tiles (optional - if you want 1-tile proximity kills) + const dx = Math.abs(this.x - player.x); + const dy = Math.abs(this.y - player.y); + + // If monster is adjacent (one tile away in any direction) + if ((dx <= 1 && dy === 0) || (dx === 0 && dy <= 1)) { + return true; + } + + return false; + } + + // Get monster state as object + getState() { + return { + x: this.x, + y: this.y + }; + } + + // Set monster state from object + setState(state) { + if (state.x !== undefined) this.x = state.x; + if (state.y !== undefined) this.y = state.y; + } +} + +module.exports = Monster; diff --git a/player.js b/player.js new file mode 100644 index 0000000..73d7346 --- /dev/null +++ b/player.js @@ -0,0 +1,48 @@ +/** + * Player class - Manages player state and actions + */ +class Player { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + this.hasKey = false; + this.hasTreasure = false; + } + + // Move player to new position + moveTo(x, y) { + this.x = x; + this.y = y; + } + + // Check if player is at given coordinates + isAt(x, y) { + return this.x === x && this.y === y; + } + + // Reset player state + reset() { + this.hasKey = false; + this.hasTreasure = false; + } + + // Get player state as object + getState() { + return { + x: this.x, + y: this.y, + hasKey: this.hasKey, + hasTreasure: this.hasTreasure + }; + } + + // Set player state from object + setState(state) { + if (state.x !== undefined) this.x = state.x; + if (state.y !== undefined) this.y = state.y; + if (state.hasKey !== undefined) this.hasKey = state.hasKey; + if (state.hasTreasure !== undefined) this.hasTreasure = state.hasTreasure; + } +} + +module.exports = Player;