From 8c0117ea884faf49e8ec52408a3fbf829245138e Mon Sep 17 00:00:00 2001 From: jwinterm Date: Fri, 23 May 2025 20:28:49 -0400 Subject: [PATCH] Fix logic and start cleanup refactor --- README.md | 2 + game.js | 24 +++-- html/index.html | 120 ++++++++++++----------- html/js/displayManager.js | 126 ++++++++++++++++++++++++ html/js/game.js | 137 +++++++++++--------------- html/js/gameState.js | 147 ++++++++++++++++++++++++++++ html/js/game_refactored.js | 139 ++++++++++++++++++++++++++ html/js/inputHandler.js | 85 ++++++++++++++++ html/js/options.js | 33 +------ html/js/screenManager.js | 195 +++++++++++++++++++++++++++++++++++++ index.js | 43 +++++++- 11 files changed, 871 insertions(+), 180 deletions(-) create mode 100644 html/js/displayManager.js create mode 100644 html/js/gameState.js create mode 100644 html/js/game_refactored.js create mode 100644 html/js/inputHandler.js create mode 100644 html/js/screenManager.js diff --git a/README.md b/README.md index 44268b6..c6fec3d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ The game is built using Node.js for the backend, with Express and Socket.io for - **Monster AI**: Evade monster as you navigate the dungeon - **Treasure hunting**: Find treasure for bonus rewards - **Wownero integration**: Players pay an entrance fee (10-100 WOW) and can win rewards +- **Chat system**: Players can communicate in real-time during gameplay +- **View other players' game states**: Spectate other players' dungeons in real-time ### Technology Stack: - Backend: Node.js with Express and Socket.io diff --git a/game.js b/game.js index 2e68745..02df8d8 100644 --- a/game.js +++ b/game.js @@ -157,7 +157,9 @@ class Game { } if (this.dungeon.treasure && newX === this.dungeon.treasure[0] && newY === this.dungeon.treasure[1] && !this.player.hasTreasure) { this.player.hasTreasure = true; - // Remove treasure from map or mark as collected + // 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 }; } @@ -214,19 +216,20 @@ class Game { // So, no conversion to relative coordinates is needed here for visibleTiles. // Just ensure this.visibleTiles is correctly populated by updateFOV. - // console.log("getState called. Player:", this.player, "Visible tiles keys:", Object.keys(this.visibleTiles || {}).length); - - return { + 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 - // items: { ...this.items }, // If you have items, send them too visibleTiles: { ...this.visibleTiles }, // Send a copy of visible tiles - // You might also want to send entrance/exit if they are always known or become known - // entrance: this.dungeon ? this.dungeon.entrance : null, - // exit: this.dungeon ? this.dungeon.exit : null, - // treasure: this.dungeon ? this.dungeon.treasure : null + entrance: this.dungeon ? this.dungeon.entrance : null, + exit: this.dungeon ? this.dungeon.exit : null, + treasure: this.dungeon ? this.dungeon.treasure : null, }; + + // Debug logging to verify entity data + console.log("🎮 getState() sending entities - Monster:", state.monster, "Entrance:", state.entrance, "Exit:", state.exit, "Treasure:", state.treasure); + + return state; } } @@ -249,4 +252,5 @@ function checkMonsterKill(player, monster) { return false; // Player is safe } -module.exports = Game; \ No newline at end of file +module.exports = Game; +module.exports.checkMonsterKill = checkMonsterKill; \ No newline at end of file diff --git a/html/index.html b/html/index.html index 8de62d3..55aecf5 100644 --- a/html/index.html +++ b/html/index.html @@ -19,7 +19,16 @@ + + + + +
@@ -110,6 +119,21 @@ scrollChat(); }); + // Handle waiting status updates + socket.on('waiting_status', 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(); + } + } + scrollChat(); + }); + socket.on('message', function(msg) { $('#messages').append($('
  • ').text(msg)); scrollChat(); @@ -120,31 +144,28 @@ console.log("🎮 Game start received with data:", data); $('#messages').append($('
  • ').text("Starting game...")); - if (typeof Game !== 'undefined') { + 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(); + if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); return; } try { - // Pass player, map, monsters, items, and initialVisibleTiles if available - var success = Game.startGame( - data.player, - data.map, - data.monsters, - data.items, - data.visibleTiles // Pass initial visible tiles from server - ); + // 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(); + 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(); updateFocusIndicator(); @@ -160,20 +181,8 @@ socket.on('game_update', function(data) { // console.log("🎮 GAME UPDATE received:", data); // Reduced verbosity - if (typeof Game !== 'undefined' && Game._gameActive) { // Ensure game is active - if (data.player) Game._player = data.player; - if (data.visibleTiles) { - Game._visibleTiles = data.visibleTiles; - Game._updateExploredTiles(); // Update explored based on new visibility - } - if (data.monster) Game._monster = data.monster; // Update monster data - if (data.items) Game._items = data.items; // Update items data - // Potentially update _entrance, _exit, _treasure if they are part of game_update - // Game._entrance = data.entrance; - // Game._exit = data.exit; - // Game._treasure = data.treasure; - - Game._drawGameScreen(); + if (typeof Game !== 'undefined' && Game._gameActive) { // Use Game._gameActive instead of gameState + Game.updateGameState(data); // Call updateGameState directly on Game object } else { // console.log("Game not active or Game undefined, skipping game_update draw."); } @@ -198,7 +207,7 @@ console.log("🔑 'ENTER' COMMAND DETECTED - expecting game_start response"); // Show waiting screen - if (typeof Game !== 'undefined') { + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } // Focus will be shifted in the 'game_start' handler after success @@ -221,11 +230,11 @@ $('#game-display').attr('tabindex', '-1'); // Make it focusable $(document).on('keydown', function(e) { - // console.log(`Keydown event: ${e.key}`, "Active element:", document.activeElement, "Game active?:", Game ? Game._gameActive : "Game undefined"); + // console.log(`Keydown event: ${e.key}`, "Active element:", document.activeElement, "Game active?:", Game && Game.gameState ? Game.gameState.gameActive : "Game undefined"); if (document.activeElement === $('#game-display')[0]) { // console.log("Game display has focus."); - if (Game && Game._gameActive) { + if (Game && Game._gameActive) { // Use Game._gameActive // console.log("Game is active. Processing key:", e.key); let dx = 0; let dy = 0; @@ -248,7 +257,7 @@ // console.log("Key pressed on game display, but not a movement key:", e.key); } } else { - console.log("Game display has focus, but Game is not active. Game object:", Game, "Game._gameActive:", Game ? Game._gameActive : "N/A"); + console.log("Game display has focus, but Game is not active. Game object:", Game, "Game._gameActive:", Game && Game.gameState ? Game.gameState.gameActive : "N/A"); } } else if (e.key === 'Enter' && document.activeElement !== $('#chatInput')[0]) { // console.log("Enter pressed, focusing chat input."); @@ -374,22 +383,10 @@ $(function() { document.getElementById('toggle-mode').addEventListener('click', function() { try { var button = document.getElementById('toggle-mode'); - // Check current mode and toggle accordingly - if (Game && Game._isTileMode) { // If currently in Tile Mode - console.log("Switching to ASCII mode..."); - if (typeof Game.switchToAsciiMode === 'function') { - Game.switchToAsciiMode(); - button.textContent = 'Switch to Tile Mode'; - } else { console.error("Game.switchToAsciiMode is not defined"); } - } else if (Game) { // If currently in ASCII mode (or Game object exists and not in tile mode) - console.log("Switching to TILE mode..."); - if (typeof Game.switchToTileMode === 'function') { - Game.switchToTileMode(); - button.textContent = 'Switch to ASCII Mode'; - } else { console.error("Game.switchToTileMode is not defined"); } - } else { - console.error("Game object not available to toggle mode."); - } + // Display mode switching has been removed - ASCII and tile modes are now equivalent + console.log("Display mode switching is no longer needed - modes are equivalent"); + button.textContent = 'Mode switching disabled'; + button.disabled = true; } catch (e) { console.error("Error switching display mode:", e); } @@ -402,15 +399,10 @@ document.getElementById('toggle-mode').addEventListener('click', function() { socket.on('queue_cancelled', function() { console.log("Queue cancelled, returning to welcome screen"); - if (typeof Game !== 'undefined') { - // Try different naming patterns for compatibility - if (typeof Game._drawWelcomeScreen === 'function') { - Game._drawWelcomeScreen(); - } else if (typeof Game.drawWelcomeScreen === 'function') { - Game.drawWelcomeScreen(); - } else { - console.error("Welcome screen function not found"); - } + if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) { + Game._drawWelcomeScreen(); + } else { + console.error("Game or _drawWelcomeScreen not available"); } }); @@ -447,16 +439,32 @@ socket.on('game_over', function(data) { $('#messages').append($('
  • ').text(message)); // Draw lose screen - Game.drawLoseScreen(reason); + if (Game && Game.drawLoseScreen) { + Game.drawLoseScreen(reason); + } } // Set game as inactive to prevent further movement - Game._gameActive = false; + if (Game) { + Game._gameActive = false; + } } scrollChat(); }); + + + diff --git a/html/js/displayManager.js b/html/js/displayManager.js new file mode 100644 index 0000000..bffac91 --- /dev/null +++ b/html/js/displayManager.js @@ -0,0 +1,126 @@ +/** + * 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/game.js b/html/js/game.js index 0f1db06..cec8ad6 100644 --- a/html/js/game.js +++ b/html/js/game.js @@ -11,7 +11,6 @@ var Game = { _display: null, _screenWidth: options.width, _screenHeight: options.height, - _isTileMode: false, // Determined after checking tileset _displayReady: false, // Flag to indicate if _display is initialized _player: null, defaultFg: "#FFF", // Define defaultFg here @@ -28,15 +27,6 @@ var Game = { console.log("Game.init called. 'this' is Game:", this === Game); console.log("Is this.checkTileset a function?", typeof this.checkTileset); - if (typeof this.checkTileset !== 'function') { - console.error("CRITICAL: Game.checkTileset is not a function. Halting initialization."); - alert("Error: Game initialization failed (checkTileset not found). Please check game.js."); - return; - } - - this.checkTileset(function(tilesetAvailable) { - console.log("Game.checkTileset callback. Tileset available: " + tilesetAvailable); - this._isTileMode = tilesetAvailable; var displayOptions = { width: this._screenWidth, @@ -44,8 +34,6 @@ var Game = { forceSquareRatio: true, // Good for both modes generally }; - if (this._isTileMode) { - console.log("Initializing display in TILE mode."); displayOptions.layout = "tile"; displayOptions.bg = window.options.bg; // Should be "transparent" displayOptions.fg = window.options.fg || "#FFF"; // Default foreground for text not in tileMap @@ -54,18 +42,6 @@ var Game = { displayOptions.tileSet = window.options.tileSet; displayOptions.tileMap = window.options.tileMap; displayOptions.tileColorize = false; // Render tiles as-is from the tileset - } else { - console.log("Initializing display in ASCII mode."); - displayOptions.layout = "rect"; - displayOptions.bg = "#000000"; // Black background for ASCII - displayOptions.fg = window.options.fg || "#FFF"; // Default foreground - displayOptions.fontSize = 18; // Adjust as needed for your font/preference - // Ensure no tile-specific options are passed if they might cause errors - delete displayOptions.tileWidth; - delete displayOptions.tileHeight; - delete displayOptions.tileSet; - delete displayOptions.tileMap; - } try { var gameDisplayContainer = document.getElementById("game-display"); @@ -90,47 +66,8 @@ var Game = { alert("Failed to initialize game display. Error: " + e.message + ". Check console for details."); this._displayReady = false; // Explicitly false on error } - }.bind(this)); // CRUCIAL: bind 'this' for the callback to refer to the Game object - }, - - checkTileset: function(callback) { - console.log("Game.checkTileset called to determine mode."); - // This function relies on options.js to have attempted loading the tileset - // and set window.tileSet, window.tileMap, and window.globalTileSetStatus. - - // Give a very short moment for options.js to potentially finish initial load attempts. - setTimeout(function() { - if (window.globalTileSetStatus === "error") { - console.warn("Tileset loading previously failed (window.globalTileSetStatus is 'error'). Using ASCII mode."); - callback(false); // Tileset not available - } else if (window.tileSet && window.tileSet.complete && window.tileSet.naturalHeight !== 0 && window.tileMap && Object.keys(window.tileMap).length > 0) { - console.log("Tileset and tileMap appear to be loaded and valid. Using TILE mode."); - callback(true); // Tileset available - } else { - // If not immediately ready, and no definitive error, try waiting a bit longer. - // options.js has its own timeouts. This is a secondary check. - console.log("Tileset not immediately confirmed. Waiting up to 3 seconds for options.js to complete loading..."); - let attempts = 0; - const maxAttempts = 6; // Try for 3 seconds (6 * 500ms) - const interval = setInterval(function() { - attempts++; - if (window.globalTileSetStatus === "error") { - clearInterval(interval); - console.warn("Tileset loading failed during interval check in Game.checkTileset. Using ASCII mode."); - callback(false); - } else if (window.tileSet && window.tileSet.complete && window.tileSet.naturalHeight !== 0 && window.tileMap && Object.keys(window.tileMap).length > 0) { - clearInterval(interval); - console.log("Tileset and tileMap confirmed after delay in Game.checkTileset. Using TILE mode."); - callback(true); - } else if (attempts >= maxAttempts) { - clearInterval(interval); - console.error("Tileset did not become available after extended wait in Game.checkTileset. Assuming not available. Using ASCII mode."); - callback(false); // Assume not available - } - }.bind(this), 500); // Check every 500ms - } - }.bind(this), 50); // Small initial delay for options.js to run - }, + }, // Removed .bind(this) - not needed for object literal methods + _ensureDisplay: function() { if (!this._display || !this._displayReady) { @@ -228,7 +165,12 @@ var Game = { console.log("Player initialized to:", this._player); this._monster = monsterData; // Assuming monsterData is structured correctly or null - this._item = itemData; // Assuming itemData is structured correctly or null + this._items = itemData; // Fixed: Use _items instead of _item + + // Initialize entrance, exit, treasure from the game data + this._entrance = null; + this._exit = null; + this._treasure = null; this._map = {}; this._visibleTiles = {}; // Clear/initialize visible tiles @@ -416,6 +358,20 @@ var Game = { this._items = data.items; // Ensure this is _items, not _item needsRedraw = true; } + + // Update entrance, exit, treasure + if (data.entrance !== undefined) { + this._entrance = data.entrance; + needsRedraw = true; + } + if (data.exit !== undefined) { + this._exit = data.exit; + needsRedraw = true; + } + if (data.treasure !== undefined) { + this._treasure = data.treasure; + needsRedraw = true; + } // Update visible tiles (most critical for movement feedback) if (data.visibleTiles && typeof data.visibleTiles === 'object' && Object.keys(data.visibleTiles).length > 0) { @@ -484,40 +440,63 @@ var Game = { if (this._visibleTiles[wy] && this._visibleTiles[wy][wx] !== undefined) { tileType = this._visibleTiles[wy][wx]; baseFg = this.defaultFg; // Use this.defaultFg - baseChar = (tileType === 1) ? '#' : (this._isTileMode ? "'" : "."); + baseChar = (tileType === 1) ? '#' : "'"; // Fixed: Always use ' for floor } else if (this._exploredTiles[wy] && this._exploredTiles[wy][wx] !== undefined) { tileType = this._exploredTiles[wy][wx]; baseFg = this.defaultFg; // Use this.defaultFg - baseChar = (tileType === 1) ? '#' : (this._isTileMode ? "'" : "."); + baseChar = (tileType === 1) ? '#' : "'"; // Fixed: Always use ' for floor } charStack.push(baseChar); fgStack.push(baseFg); bgStack.push(null); // Items, Monsters, Player drawing logic - // Example for items (ensure _item and _isVisible are defined and work correctly) - if (this._item) { // Check if _item exists - for (const itemKey in this._item) { - if (this._item.hasOwnProperty(itemKey)) { - const currentItem = this._item[itemKey]; + // Render items (fix property name from _item to _items) + if (this._items) { + for (const itemKey in this._items) { + if (this._items.hasOwnProperty(itemKey)) { + const currentItem = this._items[itemKey]; if (currentItem && currentItem.x === wx && currentItem.y === wy && this._isVisible(wx, wy)) { - charStack.push('$'); // Assuming '$' for items - fgStack.push(this.defaultFg); // Use this.defaultFg + charStack.push('$'); + fgStack.push(this.defaultFg); bgStack.push(null); } } } } - if (this._monster && this._monster.x === wx && this._monster.y === wy && this._isVisible(wx, wy)) { - charStack.push('~'); - fgStack.push(this.defaultFg); // Use this.defaultFg + // Render entrance + if (this._entrance && this._entrance[0] === wx && this._entrance[1] === wy && this._isVisible(wx, wy)) { + charStack.push('<'); + fgStack.push('#0f0'); // Green for entrance bgStack.push(null); } + // Render exit + if (this._exit && this._exit[0] === wx && this._exit[1] === wy && this._isVisible(wx, wy)) { + charStack.push('>'); + fgStack.push('#f0f'); // Magenta for exit + bgStack.push(null); + } + + // Render treasure + if (this._treasure && this._treasure[0] === wx && this._treasure[1] === wy && this._isVisible(wx, wy)) { + charStack.push('$'); + fgStack.push('#ff0'); // Yellow for treasure + bgStack.push(null); + } + + // Render monster + if (this._monster && this._monster.x === wx && this._monster.y === wy && this._isVisible(wx, wy)) { + charStack.push('~'); + fgStack.push('#f00'); // Red for monster + bgStack.push(null); + } + + // Render player (always on top) if (wx === playerWX && wy === playerWY) { charStack.push('@'); - fgStack.push(this.defaultFg); // Use this.defaultFg + fgStack.push(this.defaultFg); bgStack.push(null); } diff --git a/html/js/gameState.js b/html/js/gameState.js new file mode 100644 index 0000000..91db60b --- /dev/null +++ b/html/js/gameState.js @@ -0,0 +1,147 @@ +/** + * 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 new file mode 100644 index 0000000..495e6d4 --- /dev/null +++ b/html/js/game_refactored.js @@ -0,0 +1,139 @@ +// 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 new file mode 100644 index 0000000..9de4211 --- /dev/null +++ b/html/js/inputHandler.js @@ -0,0 +1,85 @@ +/** + * InputHandler - Manages keyboard input and translates to game actions + */ +var InputHandler = { + gameActive: false, + eventCallbacks: {}, + + init: function() { + console.log("Initializing keyboard handler"); + + const self = this; + window.addEventListener('keydown', function(e) { + self.handleKeyDown(e); + }); + + console.log("Keyboard handler initialized"); + }, + + setGameActive: function(active) { + this.gameActive = active; + }, + + on: function(eventName, callback) { + if (!this.eventCallbacks[eventName]) { + this.eventCallbacks[eventName] = []; + } + this.eventCallbacks[eventName].push(callback); + }, + + emit: function(eventName, data) { + if (this.eventCallbacks[eventName]) { + this.eventCallbacks[eventName].forEach(function(callback) { + callback(data); + }); + } + }, + + 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); + } + } +}; diff --git a/html/js/options.js b/html/js/options.js index c797587..180f645 100644 --- a/html/js/options.js +++ b/html/js/options.js @@ -23,46 +23,15 @@ tileSet.onload = function() { tileSet.onerror = function() { console.error("❌ Failed to load tileset image!"); - alert("Tileset image failed to load. Game will attempt to use ASCII mode."); window.globalTileSetStatus = "error"; // Set a status for Game.js to check if needed - - // Force ASCII mode when tileset fails to load - // REMOVED: if (typeof Game !== 'undefined' && Game._display) { - // REMOVED: Game.switchToAsciiMode(); - // REMOVED: } }; // Try different possible paths (to handle serving from different directories) tileSet.src = "tiles.png"; // Try default path first -// Add fallback function if main path fails -setTimeout(function() { - if (!tileSetLoaded) { - console.warn("Tileset not loaded yet, trying alternate paths..."); - tileSet.src = "./tiles.png"; // Try with explicit relative path - - setTimeout(function() { - if (!tileSetLoaded) { - tileSet.src = "/tiles.png"; // Try from root - - setTimeout(function() { - if (!tileSetLoaded && window.globalTileSetStatus !== "error") { // Check if error already handled - console.error("Failed to load tileset after trying multiple paths"); - alert("Could not load tileset. Game will attempt to use ASCII mode."); - window.globalTileSetStatus = "error"; // Set a status - // REMOVED: if (typeof Game !== 'undefined' && Game._display) { - // REMOVED: Game.switchToAsciiMode(); - // REMOVED: } - } - }, 1000); - } - }, 1000); - } -}, 1000); - var options = { layout: "tile", - bg: "transparent", // Black background instead of transparent for better visibility + bg: "transparent", // Adjusted dimensions for ~800x600 display width: 25, // 25 tiles * 32px/tile = 800px height: 19, // 19 tiles * 32px/tile = 608px diff --git a/html/js/screenManager.js b/html/js/screenManager.js new file mode 100644 index 0000000..8a6330b --- /dev/null +++ b/html/js/screenManager.js @@ -0,0 +1,195 @@ +/** + * 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/index.js b/index.js index f6fe5a7..1bb08c4 100644 --- a/index.js +++ b/index.js @@ -131,7 +131,11 @@ function checkGamesTimeout(currentHeight) { activeGames.forEach((game, socketId) => { const user = getUserBySocket(socketId); - if (user && user.blockRec && currentHeight > user.blockRec + 1) { + // Player must escape before the next block after their start block + // If they started on block 2, they must escape before block 3 is found + if (user && user.blockRec && currentHeight > user.blockRec) { + console.log(`Game timeout for player ${socketId}: started on block ${user.blockRec}, current block ${currentHeight}`); + // Game has timed out - player didn't escape in time game.gameState = 'lost'; io.to(socketId).emit('game_over', { @@ -241,8 +245,41 @@ io.on('connection', function(socket) { const moveResult = game.movePlayer(moveData.dx, moveData.dy); // This should update player pos and FOV if (moveResult && moveResult.status === 'moved') { - // Optional: Monster moves after player - // game.moveMonster(); + // Move monster after player moves + game.moveMonster(); + + // Check if monster caught player + const GameModule = require('./game'); + const checkMonsterKill = GameModule.checkMonsterKill || + ((player, monster) => monster.x === player.x && monster.y === player.y); + + if (checkMonsterKill(game.player, game.monster)) { + game.gameState = 'lost'; + io.to(socket.id).emit('game_over', { + status: 'lost', + reason: 'monster', + message: 'The monster caught you!' + }); + activeGames.delete(socket.id); + return; + } + + // Check for treasure pickup + if (moveResult.event === 'treasure_found') { + io.to(socket.id).emit('message', 'You found the treasure!'); + } + + // Check for escape + if (moveResult.event === 'escaped') { + game.gameState = 'won'; + io.to(socket.id).emit('game_over', { + status: 'won', + reason: 'escaped', + message: 'Congratulations! You escaped the dungeon!' + }); + activeGames.delete(socket.id); + return; + } const updatedGameState = game.getState(); // Get the new state