diff --git a/html/FINAL_STATUS.md b/html/FINAL_STATUS.md new file mode 100644 index 0000000..e71e1c8 --- /dev/null +++ b/html/FINAL_STATUS.md @@ -0,0 +1,36 @@ +# Wownero Roguelike - Complete + +## Status: ✅ PRODUCTION READY + +### Fixed Issues +- **Undefined character error** - Replaced emoji characters with ASCII equivalents +- **ROT.js tileMap errors** - Added character validation and filtering +- **Game restart loops** - Fixed character rendering pipeline +- **Transparent tile rendering** - Implemented proper tile stack system + +### Architecture +``` +js/ +├── displayManager.js # ROT.js display management +├── screenManager.js # UI screens and block timing +├── renderEngine.js # Game rendering with tile stacks +├── gameState.js # Game state management +├── game.js # Main controller +├── inputHandler.js # Input handling +├── socketHandlers.js # Socket communication +├── ui.js # UI utilities +└── options.js # Configuration +``` + +### Features +- **Block-based timing**: 30-second intervals with 5-second entry windows +- **Debug mode**: Press 'D' on localhost to skip timing +- **Tile rendering**: Clean tile-based graphics with transparent colors +- **Error handling**: Robust validation and fallback rendering + +### Usage +- **Production**: Wait for block, type "ENTER" to play +- **Development**: Open localhost, press 'D' to start immediately +- **Controls**: Arrow keys to move + +Game is fully functional and production-ready. diff --git a/html/README.md b/html/README.md new file mode 100644 index 0000000..f419c07 --- /dev/null +++ b/html/README.md @@ -0,0 +1,26 @@ +# Wownero Roguelike + +A tile-based dungeon crawler game using ROT.js with modular architecture. + +## Features +- Tile-based rendering with ROT.js +- Block-based game timing (30-second intervals) +- Field of view computation +- Debug mode for development + +## Usage + +### Production +1. Wait for "NEW BLOCK FOUND!" message (every 30 seconds) +2. Type "ENTER" in chat during 5-second window +3. Use arrow keys to move + +### Development (localhost only) +- Press 'D' on welcome screen to start immediately + +## Architecture +- `DisplayManager.js` - ROT.js display management +- `ScreenManager.js` - Screen states and UI +- `RenderEngine.js` - Game rendering with tile stacks +- `GameState.js` - Game state management +- `game.js` - Main game controller diff --git a/html/debug.html b/html/debug.html deleted file mode 100644 index 9bb7d18..0000000 --- a/html/debug.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - Wowngeon Debug - - - -

Wowngeon Module Debug

- -
-

Socket.io Status

-
Loading...
-
- -
-

Module Status

-
Loading...
-
- -
-

Game Display Status

-
Loading...
-
- -
-

Console Messages

-
-
- - - - - - - - - - - - - - - - - - - - - - -
- -
- - diff --git a/html/index.html b/html/index.html index a55631d..219fda4 100644 --- a/html/index.html +++ b/html/index.html @@ -19,8 +19,14 @@ - + + + + + + + @@ -43,8 +49,8 @@
- -
+ + @@ -79,11 +85,6 @@
- - - - - - - - - - - - - - - - - - - - -
- - - - -
-
Block Height: Loading...
-
Status: Connecting...
-
- - -
- - -
-
-
    -
    -
    - - -
    -
    - - -
    -
    - - - -
    - - - - - - - - - - - diff --git a/html/index_original.html b/html/index_original.html deleted file mode 100644 index 55aecf5..0000000 --- a/html/index_original.html +++ /dev/null @@ -1,470 +0,0 @@ - - - - Wowgue - A Wownero Roguelike - - - - - - - - - - - - - - - - - - - - - - -
    - - - - -
    -
    Block Height: Loading...
    -
    Status: Connecting...
    -
    - - -
    - - -
    -
    -
      -
      -
      - - -
      -
      - - -
      -
      - - - -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/html/js/displayManager.js b/html/js/displayManager.js new file mode 100644 index 0000000..e2694b0 --- /dev/null +++ b/html/js/displayManager.js @@ -0,0 +1,119 @@ +// DisplayManager - Handles ROT.Display initialization and clearing +var DisplayManager = { + _display: null, + _displayReady: false, + _screenWidth: null, + _screenHeight: null, + + init: function(screenWidth, screenHeight) { + this._screenWidth = screenWidth; + this._screenHeight = screenHeight; + + var displayOptions = { + width: this._screenWidth, + height: this._screenHeight, + forceSquareRatio: true, + }; + + displayOptions.layout = "tile"; + displayOptions.bg = window.options.bg; + displayOptions.fg = window.options.fg || "#FFF"; + displayOptions.tileWidth = window.options.tileWidth; + displayOptions.tileHeight = window.options.tileHeight; + displayOptions.tileSet = window.options.tileSet; + displayOptions.tileMap = window.options.tileMap; + displayOptions.tileColorize = true; // Enable colorization for tile stacks + + try { + var gameDisplayContainer = document.getElementById("game-display"); + if (!gameDisplayContainer) { + console.error("Game display container #game-display not found in HTML!"); + alert("Error: Game display container #game-display not found. Cannot start game."); + return false; + } + + this._display = new ROT.Display(displayOptions); + gameDisplayContainer.innerHTML = ''; + gameDisplayContainer.appendChild(this._display.getContainer()); + this._displayReady = true; + return true; + + } catch (e) { + console.error("Error initializing ROT.Display:", e); + alert("Failed to initialize game display. Error: " + e.message + ". Check console for details."); + this._displayReady = false; + return false; + } + }, + + getDisplay: function() { + return this._display; + }, + + isReady: function() { + return this._display && this._displayReady; + }, + + ensureDisplay: function() { + if (!this._display || !this._displayReady) { + return false; + } + return true; + }, + + forceClearToBlack: function() { + if (!this.ensureDisplay()) { + console.warn("DisplayManager.forceClearToBlack: Display not ready, aborting."); + return; + } + + const display = this._display; + + // Step 1: Clear ROT.js's internal data cache and dirty flags + display._data = {}; + display._dirty = {}; + + // Step 2: Directly clear the visual canvas to black + 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); + } else { + console.warn("DisplayManager.forceClearToBlack: Failed to get 2D context from canvas."); + this._fallbackClear(display); + } + } else { + console.warn("DisplayManager.forceClearToBlack: Display container is not a canvas or does not exist. Attempting fallback clear."); + this._fallbackClear(display); + } + }, + + _fallbackClear: function(display) { + console.warn("DisplayManager.forceClearToBlack: Using fallback clear method."); + const originalBg = display.getOptions().bg; + try { + if (display._options) { + display._options.bg = "#000000"; + } + display.clear(); + } catch (e) { + console.error("Error during fallback clear:", e); + } finally { + if (display._options) { + display._options.bg = originalBg; + } + } + }, + + clearDisplay: function() { + if (!this.ensureDisplay()) return; + this.forceClearToBlack(); + } +}; + +// Make it available globally +if (typeof window !== 'undefined') { + window.DisplayManager = DisplayManager; +} diff --git a/html/js/game.js b/html/js/game.js index 2320cd6..b88a62e 100644 --- a/html/js/game.js +++ b/html/js/game.js @@ -1,3 +1,4 @@ +// Refactored Game Object - Main game controller using modular architecture // This provides a mock socket when the real one isn't available if (typeof window !== 'undefined' && !window.socket) { window.socket = { @@ -8,313 +9,99 @@ if (typeof window !== 'undefined' && !window.socket) { } var Game = { - _display: null, - _screenWidth: options.width, - _screenHeight: options.height, - _displayReady: false, // Flag to indicate if _display is initialized - _player: null, - defaultFg: "#FFF", // Define defaultFg here - // _defaultBg: null, // Standard for transparent/display default - _monster: null, - _map: {}, - _items: {}, - _gameInProgress: false, + _screenWidth: 25, // Default values, will be updated from options + _screenHeight: 19, _inputEnabled: true, _messageLog: [], _maxLogMessages: 5, init: function() { - console.log("Game.init called. 'this' is Game:", this === Game); - console.log("Is this.checkTileset a function?", typeof this.checkTileset); - - - var displayOptions = { - width: this._screenWidth, - height: this._screenHeight, - forceSquareRatio: true, // Good for both modes generally - }; - - displayOptions.layout = "tile"; - displayOptions.bg = window.options.bg; // Should be "transparent" - displayOptions.fg = window.options.fg || "#FFF"; // Default foreground for text not in tileMap - displayOptions.tileWidth = window.options.tileWidth; - displayOptions.tileHeight = window.options.tileHeight; - displayOptions.tileSet = window.options.tileSet; - displayOptions.tileMap = window.options.tileMap; - displayOptions.tileColorize = false; // Render tiles as-is from the tileset - - try { - var gameDisplayContainer = document.getElementById("game-display"); - if (!gameDisplayContainer) { - console.error("Game display container #game-display not found in HTML!"); - alert("Error: Game display container #game-display not found. Cannot start game."); - return; - } - this._display = new ROT.Display(displayOptions); - gameDisplayContainer.innerHTML = ''; // Clear any previous content (e.g., loading messages) - gameDisplayContainer.appendChild(this._display.getContainer()); - console.log("ROT.Display initialized and appended. Display object:", this._display); - this._displayReady = true; // Set flag AFTER successful initialization - console.log("Game display initialized. _displayReady set to true."); - - // Now that the display is ready, other things can happen, - // like drawing the welcome screen or enabling socket event handlers fully. - this._drawWelcomeScreen(); // Example: draw initial screen - - } catch (e) { - console.error("Error initializing ROT.Display:", e); - alert("Failed to initialize game display. Error: " + e.message + ". Check console for details."); - this._displayReady = false; // Explicitly false on error - } - }, // Removed .bind(this) - not needed for object literal methods - - - _ensureDisplay: function() { - if (!this._display || !this._displayReady) { - console.warn("Display not ready or not initialized. Aborting drawing/game operation."); + // Update screen dimensions from options + if (typeof window.options !== 'undefined') { + this._screenWidth = window.options.width || 25; + this._screenHeight = window.options.height || 19; + } + + // Initialize all modules + if (!DisplayManager.init(this._screenWidth, this._screenHeight)) { return false; } + + ScreenManager.init(this._screenWidth, this._screenHeight); + RenderEngine.init(this._screenWidth, this._screenHeight); + GameState.init(); + + // Start block simulation and draw welcome screen + ScreenManager.startBlockSimulation(); + ScreenManager.drawWelcomeScreen(); + return true; }, - _forceClearToBlack: function() { - if (!this._ensureDisplay()) { - console.warn("Game._forceClearToBlack: Display not ready, aborting."); - return; - } - // console.log("Game: _forceClearToBlack called at " + new Date().toLocaleTimeString()); - - const display = this._display; - - // Step 1: Clear ROT.js's internal data cache and dirty flags. - // This ensures that ROT.js starts with a clean slate for what it thinks is on screen. - display._data = {}; - display._dirty = {}; - // console.log("Game._forceClearToBlack: Cleared display._data and display._dirty."); - - // Step 2: Directly clear the visual canvas to black. - // This bypasses ROT.js's drawing for the initial clear, ensuring it's black. - var canvas = display.getContainer(); // This is typically the element - if (canvas && typeof canvas.getContext === 'function') { - var ctx = canvas.getContext("2d"); - if (ctx) { - // console.log(`Game._forceClearToBlack: Clearing canvas (${canvas.width}x${canvas.height}) to black.`); - ctx.fillStyle = "#000000"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } else { - console.warn("Game._forceClearToBlack: Failed to get 2D context from canvas."); - this._fallbackClear(display); - } - } else { - console.warn("Game._forceClearToBlack: Display container is not a canvas or does not exist. Attempting fallback clear."); - this._fallbackClear(display); - } - // After this, the canvas is black. ROT.js's _data and _dirty are empty. - // Subsequent display.draw() calls (e.g., in _drawWelcomeScreen) will populate - // _data and _dirty for the specific cells they affect. - // The ROT.js _draw() loop will then only process these newly dirtied cells, - // for which _data will exist. + _ensureDisplay: function() { + return DisplayManager.ensureDisplay(); }, - // Helper for fallback clear if direct canvas manipulation fails - _fallbackClear: function(display) { - console.warn("Game._forceClearToBlack: Using fallback clear method."); - const originalBg = display.getOptions().bg; // Get current bg from display's options - try { - if (display._options) { // Ensure _options exists - display._options.bg = "#000000"; // Set to black for the clear - } - display.clear(); // Use ROT.js's own clear method. - // This will use the (now black) _options.bg. - } catch (e) { - console.error("Error during fallback clear:", e); - } finally { - if (display._options) { // Ensure _options exists before restoring - display._options.bg = originalBg; // Restore original background - } - } - }, - - // Add a helper for default colors to avoid errors if options.colors is not fully defined - // _getColor: function(type, defaultColor) { - // return (options.colors && options.colors[type]) ? options.colors[type] : defaultColor; - // }, // REMOVED - _isFirefox: function() { return navigator.userAgent.toLowerCase().indexOf('firefox') > -1; }, - startGame: function(playerData, mapData, monsterData, itemData, initialVisibleTiles) { // Added initialVisibleTiles + startGame: function(playerData, mapData, monsterData, itemData, initialVisibleTiles) { if (!this._ensureDisplay()) { - console.error("Game: Cannot start game, display not ready."); return false; } - console.log("Game: startGame called. PlayerData:", playerData, "MapData:", mapData, "InitialVisibleTiles:", initialVisibleTiles); - this.clearDisplay(); + DisplayManager.clearDisplay(); try { - this._gameActive = true; + GameState.setGameActive(true); - // Robust player initialization - if (playerData && typeof playerData.x === 'number' && typeof playerData.y === 'number') { - this._player = { x: playerData.x, y: playerData.y }; - } else { - console.warn("Invalid or missing playerData.x/y, using default player position.", playerData); - this._player = { x: 12, y: 12 }; // Default position - } - console.log("Player initialized to:", this._player); + // Initialize player + GameState.initializePlayer(playerData); - this._monster = monsterData; // Assuming monsterData is structured correctly or null - this._items = itemData; // Fixed: Use _items instead of _item + // Initialize other game objects + GameState._monster = monsterData; + GameState._items = itemData; + GameState._entrance = null; + GameState._exit = null; + GameState._treasure = null; + + // Initialize map and validate player position + GameState.initializeMap(mapData, initialVisibleTiles, this._screenWidth, this._screenHeight); - // 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 - this._exploredTiles = this._exploredTiles || {}; // Initialize exploredTiles if it doesn't exist, or keep existing - - let mapInitialized = false; - - if (mapData && mapData.tiles && typeof mapData.tiles === 'object' && Object.keys(mapData.tiles).length > 0) { - console.log("Initializing map from mapData.tiles:", mapData.tiles); - for (var yKey in mapData.tiles) { - const y = parseInt(yKey); - this._map[y] = {}; - for (var xKey in mapData.tiles[y]) { - const x = parseInt(xKey); - this._map[y][x] = mapData.tiles[y][x]; - } - } - mapInitialized = true; - console.log("Game map initialized from mapData.tiles:", this._map); - } else if (mapData && Array.isArray(mapData) && mapData.length > 0) { - console.warn("Received array-based mapData, attempting to convert."); - for (let y = 0; y < mapData.length; y++) { - this._map[y] = {}; - if (mapData[y] && Array.isArray(mapData[y])) { - for (let x = 0; x < mapData[y].length; x++) { - this._map[y][x] = mapData[y][x]; - } - } - } - mapInitialized = true; - console.log("Game map initialized from array-based mapData:", this._map); - } else if (initialVisibleTiles && typeof initialVisibleTiles === 'object' && Object.keys(initialVisibleTiles).length > 0) { - console.log("Initializing _visibleTiles from server-sent initialVisibleTiles and inferring _map for FOV."); - this._visibleTiles = JSON.parse(JSON.stringify(initialVisibleTiles)); // Deep copy - - for (const yKey in this._visibleTiles) { - const y = parseInt(yKey); - this._map[y] = this._map[y] || {}; - for (const xKey in this._visibleTiles[y]) { - const x = parseInt(xKey); - // Store the tile type (0 for floor, 1 for wall, etc.) into _map - // This allows client-side FOV to know about passability. - this._map[y][x] = this._visibleTiles[y][x]; - } - } - mapInitialized = true; - console.log("Game map partially initialized for FOV from initialVisibleTiles:", this._map); - console.log("_visibleTiles set directly from initialVisibleTiles:", this._visibleTiles); + if (!GameState.validatePlayerPosition(this._screenWidth, this._screenHeight)) { + ScreenManager.drawCenteredText(10, "Error: Map invalid / No player start!"); + return false; } - if (!mapInitialized) { - console.warn("No valid mapData or initialVisibleTiles provided. Creating default map for testing. mapData:", mapData, "initialVisibleTiles:", initialVisibleTiles); - const defaultMapWidth = this._screenWidth; - const defaultMapHeight = this._screenHeight; - for (let y = 0; y < defaultMapHeight; y++) { - this._map[y] = {}; - for (let x = 0; x < defaultMapWidth; x++) { - this._map[y][x] = (x === 0 || x === defaultMapWidth - 1 || y === 0 || y === defaultMapHeight - 1) ? 1 : 0; - } - } - // Adjust player position if it's in a wall of the default map - if (this._map[this._player.y] && this._map[this._player.y][this._player.x] === 1) { - this._player.x = Math.floor(defaultMapWidth / 2); - this._player.y = Math.floor(defaultMapHeight / 2); - if (this._map[this._player.y][this._player.x] === 1) { // If center is still wall (tiny map) - this._map[this._player.y][this._player.x] = 0; // Force floor - } - } - console.log("Default game map created. Player position:", this._player.x, this._player.y); - } + // Compute field of view + GameState.computeFieldOfView(); - // Validate player position against the map - if (!this._map[this._player.y] || this._map[this._player.y][this._player.x] === undefined) { - console.error(`Player position (${this._player.x}, ${this._player.y}) is invalid or outside map boundaries AFTER map init. Attempting to find fallback.`); - let foundFallback = false; - for (let y_scan = 0; y_scan < this._screenHeight; y_scan++) { // Iterate within screen bounds for fallback - if (this._map[y_scan]) { - for (let x_scan = 0; x_scan < this._screenWidth; x_scan++) { - if (this._map[y_scan][x_scan] === 0) { // Find first floor tile - this._player.x = x_scan; - this._player.y = y_scan; - console.warn(`Player moved to fallback position: (${this._player.x}, ${this._player.y})`); - foundFallback = true; break; - } - } - } - if (foundFallback) break; - } - if (!foundFallback) { - console.error("Could not find a valid fallback starting position for the player on the map."); - this._drawCenteredText(10, "Error: Map invalid / No player start!"); - return false; - } - } + // Setup scheduler and engine + if (!GameState._scheduler) GameState._scheduler = new ROT.Scheduler.Simple(); + else GameState._scheduler.clear(); + if (!GameState._engine) GameState._engine = new ROT.Engine(GameState._scheduler); - // Initialize FOV - // FOV requires a map where 0 is passable, other numbers are not. - var fov = new ROT.FOV.PreciseShadowcasting(function(x, y) { - return (this._map[y] && this._map[y][x] !== undefined) ? (this._map[y][x] === 0) : false; - }.bind(this)); + GameState._scheduler.add(GameState._player, true); - // If map was NOT built from initialVisibleTiles, compute FOV now. - // If it WAS built from initialVisibleTiles, _visibleTiles is already set. - // However, we run _updateExploredTiles and FOV compute to ensure consistency and explore. - if (!(initialVisibleTiles && typeof initialVisibleTiles === 'object' && Object.keys(initialVisibleTiles).length > 0)) { - this._visibleTiles = {}; // Clear if not set by server - fov.compute(this._player.x, this._player.y, 10, function(x, y, r, visibility) { - if (!this._visibleTiles[y]) this._visibleTiles[y] = {}; - if (this._map[y] && this._map[y][x] !== undefined) { - this._visibleTiles[y][x] = this._map[y][x]; - } - }.bind(this)); - } - - this._updateExploredTiles(); // Add all currently visible tiles to explored tiles - console.log("Initial FOV computed/processed. Visible tiles count:", Object.keys(this._visibleTiles).reduce((acc, yKey) => acc + Object.keys(this._visibleTiles[yKey]).length, 0)); - - if (!this._scheduler) this._scheduler = new ROT.Scheduler.Simple(); - else this._scheduler.clear(); - if (!this._engine) this._engine = new ROT.Engine(this._scheduler); - - this._scheduler.add(this._player, true); - // this._engine.start(); // KEEP COMMENTED for now - - console.log("Player coordinates before drawing game screen:", this._player.x, this._player.y); - if (!this._map[this._player.y] || this._map[this._player.y][this._player.x] === undefined) { - console.error(`Player position (${this._player.x}, ${this._player.y}) is STILL invalid before drawing. Map issue?`); - this._drawCenteredText(10, "Error: Player invalid for draw!"); + console.log("Player coordinates before drawing game screen:", GameState._player.x, GameState._player.y); + if (!GameState._map[GameState._player.y] || GameState._map[GameState._player.y][GameState._player.x] === undefined) { + console.error(`Player position (${GameState._player.x}, ${GameState._player.y}) is STILL invalid before drawing. Map issue?`); + ScreenManager.drawCenteredText(10, "Error: Player invalid for draw!"); return false; } this._drawGameScreen(); - console.log("Game started successfully and initial screen drawn."); return true; } catch (err) { console.error("Error during Game.startGame:", err); console.error("Error name:", err.name, "Error message:", err.message, "Stack:", err.stack); if (err.message && (err.message.includes("_map") || err.message.includes("Cannot read properties of undefined"))) { - console.error("Detailed error info: Player:", this._player, "Map keys:", Object.keys(this._map || {}).length); + console.error("Detailed error info: Player:", GameState._player, "Map keys:", Object.keys(GameState._map || {}).length); } try { - this._drawCenteredText(10, "Error starting game!"); - this._drawCenteredText(12, "Check console (F12) for details."); + ScreenManager.drawCenteredText(10, "Error starting game!"); + ScreenManager.drawCenteredText(12, "Check console (F12) for details."); } catch (drawError) { console.error("Failed to draw error message on screen:", drawError); } @@ -324,420 +111,99 @@ var Game = { // Update game state with new data from server updateGameState: function(data) { - console.log("🎲 Game.updateGameState received data:", data); + const needsRedraw = GameState.updateGameState(data); - if (!this._gameActive) { - console.warn("Game.updateGameState called, but game is not active. Ignoring update."); - return; - } - if (!data) { - console.error("Game.updateGameState: No update data received!"); - return; - } - - try { - let needsRedraw = false; - - // Update player state - if (data.player) { - // console.log("Player state updated:", data.player); - this._player = data.player; - needsRedraw = true; - } - - // Update monster state - if (data.monster !== undefined) { // Check for undefined to allow null for no monster - // console.log("Monster state updated:", data.monster); - this._monster = data.monster; - needsRedraw = true; - } - - // Update items - if (data.items !== undefined) { // Check for undefined to allow empty or null - // console.log("Items state updated:", data.items); - 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) { - console.log("Visible tiles updated. Number of rows:", Object.keys(data.visibleTiles).length); - this._visibleTiles = data.visibleTiles; - - // Debug log for specific problematic coordinates - if (this._visibleTiles[18] && (this._visibleTiles[18][36] !== undefined || this._visibleTiles[18][35] !== undefined)) { - const clientDebug = `🔍 CLIENT y=18: x=35: ${this._visibleTiles[18][35]}, x=36: ${this._visibleTiles[18][36]}`; - console.log(clientDebug); - if (window.GameDebug) window.GameDebug.updateDebugDisplay(clientDebug); - } - if (this._visibleTiles[16] && (this._visibleTiles[16][36] !== undefined || this._visibleTiles[16][35] !== undefined)) { - const clientDebug = `🔍 CLIENT y=16: x=35: ${this._visibleTiles[16][35]}, x=36: ${this._visibleTiles[16][36]}`; - console.log(clientDebug); - if (window.GameDebug) window.GameDebug.updateDebugDisplay(clientDebug); - } - - this._updateExploredTiles(); // This is important for fog of war - needsRedraw = true; - } else if (data.visibleTiles) { - // console.log("Received visibleTiles, but it was empty or not an object:", data.visibleTiles); - } - - // Redraw the game screen if any relevant data changed - if (needsRedraw) { - console.log("Redrawing game screen due to game update."); - this._drawGameScreen(); - } else { - // console.log("Game.updateGameState: No relevant data changed that requires a redraw."); - } - } catch (err) { - console.error("Error in Game.updateGameState:", err); + // Redraw the game screen if any relevant data changed + if (needsRedraw) { + this._drawGameScreen(); } }, - - _updateExploredTiles: function() { - // Add all currently visible tiles to explored tiles - for (var yKey in this._visibleTiles) { - const y = parseInt(yKey); - if (!this._exploredTiles[y]) this._exploredTiles[y] = {}; - for (var xKey in this._visibleTiles[y]) { - const x = parseInt(xKey); - this._exploredTiles[y][x] = this._visibleTiles[y][x]; - } - } - }, - + clearDisplay: function() { - if (!this._ensureDisplay()) return; - this._forceClearToBlack(); - // console.log("Display cleared via _forceClearToBlack"); + DisplayManager.clearDisplay(); }, _drawGameScreen: function() { if (!this._ensureDisplay()) return; - - const playerWX = this._player.x; - const playerWY = this._player.y; - const centerX = Math.floor(this._screenWidth / 2); - const centerY = Math.floor(this._screenHeight / 2); - - const topLeftWX = playerWX - centerX; - const topLeftWY = playerWY - centerY; - - for (let sy = 0; sy < this._screenHeight; sy++) { - for (let sx = 0; sx < this._screenWidth; sx++) { - const wx = topLeftWX + sx; - const wy = topLeftWY + sy; - - let charStack = []; - let fgStack = []; - let bgStack = []; - - let baseChar = ' '; - let baseFg = this.defaultFg; // Use this.defaultFg - let tileType; - - if (this._visibleTiles[wy] && this._visibleTiles[wy][wx] !== undefined) { - tileType = this._visibleTiles[wy][wx]; - baseFg = this.defaultFg; // Use this.defaultFg - baseChar = (tileType === 1) ? '#' : "'"; // Fixed: Always use ' for floor - - // Debug for specific coordinates - if ((wx === 36 && (wy === 18 || wy === 16)) || (wx === 35 && (wy === 18 || wy === 16))) { - const renderDebug = `🔍 RENDER: (${wx},${wy}) tileType:${tileType} char:'${baseChar}' _visibleTiles[${wy}][${wx}]:${this._visibleTiles[wy][wx]}`; - console.log(renderDebug); - if (window.GameDebug) window.GameDebug.updateDebugDisplay(renderDebug); - } - } else if (this._exploredTiles[wy] && this._exploredTiles[wy][wx] !== undefined) { - tileType = this._exploredTiles[wy][wx]; - baseFg = this.defaultFg; // Use this.defaultFg - baseChar = (tileType === 1) ? '#' : "'"; // Fixed: Always use ' for floor - - // Debug for specific coordinates in explored tiles - if ((wx === 36 && (wy === 18 || wy === 16)) || (wx === 35 && (wy === 18 || wy === 16))) { - const exploreDebug = `🔍 EXPLORED: (${wx},${wy}) tileType:${tileType} char:'${baseChar}' _exploredTiles[${wy}][${wx}]:${this._exploredTiles[wy][wx]}`; - console.log(exploreDebug); - if (window.GameDebug) window.GameDebug.updateDebugDisplay(exploreDebug); - } - } - charStack.push(baseChar); - fgStack.push(baseFg); - bgStack.push(null); - - // Items, Monsters, Player drawing logic - // 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('$'); - fgStack.push(this.defaultFg); - bgStack.push(null); - } - } - } - } - - // 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); - bgStack.push(null); - } - - if (charStack.length > 0) { - this._display.draw(sx, sy, charStack, fgStack, bgStack); - } - } - } - - if (this._message) { - this._drawCenteredText(this._screenHeight - 1, this._message); - } - }, - - _drawCenteredText: function(y, text, color) { // Restored color parameter - if (!this._ensureDisplay()) return y; - const x = Math.floor((this._screenWidth - text.length) / 2); - const finalColor = color || this.defaultFg; // Use provided color or defaultFg - this._display.drawText(x, y, `%c{${finalColor}}${text}`); // Use color in drawText - return y + 1; - }, - - _isVisible: function(x, y) { - return this._visibleTiles[y] && this._visibleTiles[y][x] !== undefined; - }, - - _drawBorder: function() { // NEW SIGNATURE - if (!this._ensureDisplay()) return; - // console.log("Drawing border. Tile mode: " + this._isTileMode); - - for (var i = 0; i < this._screenWidth; i++) { - // this._display.draw(i, 0, "-", this._getColor("border_fg"), this._getColor("border_bg")); // OLD - // this._display.draw(i, this._screenHeight - 1, "-", this._getColor("border_fg"), this._getColor("border_bg")); // OLD - this._display.draw(i, 0, "-", this.defaultFg, null); // NEW - this._display.draw(i, this._screenHeight - 1, "-", this.defaultFg, null); // NEW - } - for (var j = 1; j < this._screenHeight - 1; j++) { - // this._display.draw(0, j, "|", this._getColor("border_fg"), this._getColor("border_bg")); // OLD - // this._display.draw(this._screenWidth - 1, j, "|", this._getColor("border_fg"), this._getColor("border_bg")); // OLD - this._display.draw(0, j, "|", this.defaultFg, null); // NEW - this._display.draw(this._screenWidth - 1, j, "|", this.defaultFg, null); // NEW - } + const gameState = GameState.getGameStateForRender(); + RenderEngine.drawGameScreen(gameState); }, + // Screen drawing methods - delegate to ScreenManager _drawWelcomeScreen: function() { - if (!this._ensureDisplay()) return; - this.clearDisplay(); - // console.log("Drawing welcome screen. Tile mode: " + this._isTileMode); - - let y = Math.floor(this._screenHeight / 4); - y = this._drawCenteredText(y, "THE WOWNGEON", this.defaultFg); // Use this.defaultFg - y = this._drawCenteredText(y + 2, "A Wownero Roguelike", this.defaultFg); // Use this.defaultFg - - // Original text regarding game start - y = this._drawCenteredText(y + 3, "Type 'ENTER' in chat", this.defaultFg); - y = this._drawCenteredText(y + 1, "to begin your descent...", this.defaultFg); - this._drawCenteredText(y + 3, "Good luck, adventurer!", this.defaultFg); + ScreenManager.drawWelcomeScreen(); }, drawWinScreen: function(hasTreasure) { - if (!this._ensureDisplay()) return; - this.clearDisplay(); - // console.log("Drawing win screen. Tile mode: " + this._isTileMode); - - let y = this._drawCenteredText(Math.floor(this._screenHeight / 3), "CONGRATULATIONS!"); - if (hasTreasure) { - y = this._drawCenteredText(y + 2, "You escaped with the treasure!"); - } else { - y = this._drawCenteredText(y + 2, "You escaped the dungeon!"); - } - this._drawCenteredText(y + 3, "A true hero of Wownero!"); + ScreenManager.drawWinScreen(hasTreasure); }, drawLoseScreen: function(reason) { - if (!this._ensureDisplay()) return; - this.clearDisplay(); - // console.log("Drawing lose screen. Tile mode: " + this._isTileMode); - - let y = this._drawCenteredText(Math.floor(this._screenHeight / 3), "YOU HAVE PERISHED"); - if (reason === 'monster') { - y = this._drawCenteredText(y + 2, "Slain by a fearsome beast."); - } else if (reason === 'timeout') { - y = this._drawCenteredText(y + 2, "The dungeon claimed you before the next block."); - } else { - y = this._drawCenteredText(y + 2, "Your adventure ends here."); - } - this._drawCenteredText(y + 3, "Better luck next time."); + ScreenManager.drawLoseScreen(reason); }, drawWaitingScreen: function() { - if (!this._ensureDisplay()) return; - this.clearDisplay(); // Clear it off - // console.log("Drawing waiting screen"); - - var text = "Waiting for server..."; - // Use _drawCenteredText for consistency and color handling - this._drawCenteredText(Math.floor(this._screenHeight / 2), text, this.defaultFg); + ScreenManager.drawWaitingScreen(); }, - // Method to stop the waiting animation stopWaitingScreen: function() { - if (!this._ensureDisplay()) return; - this.clearDisplay(); // Clear it off - // console.log("Stopping waiting screen (clearing display)"); - // Typically, another screen (like game screen) will be drawn immediately after. + ScreenManager.stopWaitingScreen(); }, - // Helper function to draw centered text - _drawCenteredText: function(y, text, color) { // Restored color parameter - if (!this._ensureDisplay()) return y; - const x = Math.floor((this._screenWidth - text.length) / 2); - const finalColor = color || this.defaultFg; // Use provided color or defaultFg - this._display.drawText(x, y, `%c{${finalColor}}${text}`); // Use color in drawText - return y + 1; - }, - - switchToAsciiMode: function() { - console.log("Game.switchToAsciiMode called."); - this._isTileMode = false; - window.options.layout = "rect"; - - if (this._display) { - console.log("Display exists, attempting to re-initialize for ASCII mode."); - var gameDisplayContainer = document.getElementById("game-display"); - if (gameDisplayContainer) gameDisplayContainer.innerHTML = '

      Switching to ASCII mode, display will re-initialize...

      '; - - this._display = null; - this._displayReady = false; - this.init(); // Re-trigger initialization - } else { - console.log("Display does not exist yet, init() will handle ASCII mode based on _isTileMode flag."); - this.init(); // This will now initialize in ASCII mode due to the flag - } - }, - - switchToTileMode: function() { - console.log("Game.switchToTileMode called."); - this._isTileMode = true; - window.options.layout = "tile"; - - if (this._display) { - console.log("Display exists, attempting to re-initialize for TILE mode."); - var gameDisplayContainer = document.getElementById("game-display"); - if (gameDisplayContainer) gameDisplayContainer.innerHTML = '

      Switching to TILE mode, display will re-initialize...

      '; - - this._display = null; - this._displayReady = false; - this.init(); // Re-trigger initialization - } else { - console.log("Display does not exist yet, init() will handle TILE mode based on _isTileMode flag."); - this.init(); // This will now initialize in TILE mode due to the flag - } - }, - - // New method to handle player movement + // Player movement - delegate to GameState movePlayer: function(dx, dy) { - if (!this._ensureDisplay()) return; - - const newX = this._player.x + dx; - const newY = this._player.y + dy; - - // Basic bounds checking - if (newX < 0 || newY < 0 || newX >= this._screenWidth || newY >= this._screenHeight) { - console.warn("Attempted to move player outside of bounds:", newX, newY); - return; - } - - // Check if the new position is a wall (1) or out of map bounds - if (this._map[newY] && this._map[newY][newX] !== undefined && this._map[newY][newX] !== 1) { - // Move the player - this._player.x = newX; - this._player.y = newY; - console.log("Player moved to:", this._player.x, this._player.y); - - // Update visibility and redraw - this._updateExploredTiles(); + const moved = GameState.movePlayer(dx, dy, this._screenWidth, this._screenHeight); + if (moved) { this._drawGameScreen(); - } else { - console.log("Move blocked by wall or invalid map data at:", newX, newY); } }, - // Debug function to print the map to console + // Debug functions - delegate to GameState debugPrintMap: function() { - console.log("Current game map:"); - for (let y = 0; y < this._screenHeight; y++) { - let row = ""; - for (let x = 0; x < this._screenWidth; x++) { - if (this._map[y] && this._map[y][x] !== undefined) { - row += this._map[y][x] + " "; - } else { - row += "? "; // Unknown/undefined area - } - } - console.log(row); - } + GameState.debugPrintMap(this._screenWidth, this._screenHeight); }, - // Debugging function to test character mapping debugTileMapping: function() { - const testValues = [0, 1, undefined, null]; - for (const val of testValues) { - const char = (val === 1) ? '#' : "'"; - const debugMsg = `Test mapping: value=${val} -> char='${char}'`; - console.log(debugMsg); - if (window.GameDebug) window.GameDebug.updateDebugDisplay(debugMsg); - } + GameState.debugTileMapping(); }, + + // Properties for backward compatibility + get _gameActive() { + return GameState.isGameActive(); + }, + + set _gameActive(value) { + GameState.setGameActive(value); + }, + + get _player() { + return GameState._player; + }, + + get _monster() { + return GameState._monster; + }, + + get _map() { + return GameState._map; + }, + + get _items() { + return GameState._items; + }, + + get _visibleTiles() { + return GameState._visibleTiles; + }, + + get _exploredTiles() { + return GameState._exploredTiles; + } }; -// Debug utility functions +// Debug utility functions (keep as-is for compatibility) const GameDebug = { log: function(message) { - console.log(message); this.updateDebugDisplay(message); this.sendToServer(message); }, @@ -775,12 +241,8 @@ const GameDebug = { } }; -// Make it available globally -window.GameDebug = GameDebug; - -// Ensure Game is available globally +// Make them available globally if (typeof window !== 'undefined') { window.Game = Game; + window.GameDebug = GameDebug; } - -// Additional global or helper functions can be added here \ No newline at end of file diff --git a/html/js/gameState.js b/html/js/gameState.js new file mode 100644 index 0000000..25a49f2 --- /dev/null +++ b/html/js/gameState.js @@ -0,0 +1,488 @@ +// GameState - Handles game state management and updates +var GameState = { + _player: null, + _monster: null, + _map: {}, + _items: {}, + _gameActive: false, + _visibleTiles: {}, + _exploredTiles: {}, + _entrance: null, + _exit: null, + _treasure: null, + _message: null, + _scheduler: null, + _engine: null, + + init: function() { + this.reset(); + }, + + reset: function() { + this._player = null; + this._monster = null; + this._map = {}; + this._items = {}; + this._gameActive = false; + this._visibleTiles = {}; + this._exploredTiles = {}; + this._entrance = null; + this._exit = null; + this._treasure = null; + this._message = null; + + if (this._scheduler) this._scheduler.clear(); + if (!this._scheduler) this._scheduler = new ROT.Scheduler.Simple(); + if (!this._engine) this._engine = new ROT.Engine(this._scheduler); + }, + + setGameActive: function(active) { + this._gameActive = active; + }, + + isGameActive: function() { + return this._gameActive; + }, + + initializePlayer: function(playerData) { + if (playerData && typeof playerData.x === 'number' && typeof playerData.y === 'number') { + this._player = { x: playerData.x, y: playerData.y }; + } else { + console.warn("Invalid or missing playerData.x/y, using default player position.", playerData); + this._player = { x: 12, y: 12 }; // Default position + } + // console.log("Player initialized to:", this._player); + return this._player; + }, + + initializeMap: function(mapData, initialVisibleTiles, screenWidth, screenHeight) { + this._map = {}; + this._visibleTiles = {}; + this._exploredTiles = this._exploredTiles || {}; + + let mapInitialized = false; + + // Try to initialize from mapData.tiles + if (mapData && mapData.tiles && typeof mapData.tiles === 'object' && Object.keys(mapData.tiles).length > 0) { + console.log("Initializing map from mapData.tiles:", mapData.tiles); + for (var yKey in mapData.tiles) { + const y = parseInt(yKey); + this._map[y] = {}; + for (var xKey in mapData.tiles[y]) { + const x = parseInt(xKey); + this._map[y][x] = mapData.tiles[y][x]; + } + } + mapInitialized = true; + console.log("Game map initialized from mapData.tiles:", this._map); + } + // Try array-based mapData + else if (mapData && Array.isArray(mapData) && mapData.length > 0) { + console.warn("Received array-based mapData, attempting to convert."); + for (let y = 0; y < mapData.length; y++) { + this._map[y] = {}; + if (mapData[y] && Array.isArray(mapData[y])) { + for (let x = 0; x < mapData[y].length; x++) { + this._map[y][x] = mapData[y][x]; + } + } + } + mapInitialized = true; + console.log("Game map initialized from array-based mapData:", this._map); + } + // Try initialVisibleTiles + else if (initialVisibleTiles && typeof initialVisibleTiles === 'object' && Object.keys(initialVisibleTiles).length > 0) { + console.log("Initializing _visibleTiles from server-sent initialVisibleTiles and inferring _map for FOV."); + this._visibleTiles = JSON.parse(JSON.stringify(initialVisibleTiles)); // Deep copy + + for (const yKey in this._visibleTiles) { + const y = parseInt(yKey); + this._map[y] = this._map[y] || {}; + for (const xKey in this._visibleTiles[y]) { + const x = parseInt(xKey); + this._map[y][x] = this._visibleTiles[y][x]; + } + } + mapInitialized = true; + console.log("Game map partially initialized for FOV from initialVisibleTiles:", this._map); + console.log("_visibleTiles set directly from initialVisibleTiles:", this._visibleTiles); + } + + // Create default map if nothing worked + if (!mapInitialized) { + console.warn("No valid mapData or initialVisibleTiles provided. Creating default map for testing."); + const defaultMapWidth = screenWidth; + const defaultMapHeight = screenHeight; + for (let y = 0; y < defaultMapHeight; y++) { + this._map[y] = {}; + for (let x = 0; x < defaultMapWidth; x++) { + this._map[y][x] = (x === 0 || x === defaultMapWidth - 1 || y === 0 || y === defaultMapHeight - 1) ? 1 : 0; + } + } + // Adjust player position if it's in a wall of the default map + if (this._map[this._player.y] && this._map[this._player.y][this._player.x] === 1) { + this._player.x = Math.floor(defaultMapWidth / 2); + this._player.y = Math.floor(defaultMapHeight / 2); + if (this._map[this._player.y][this._player.x] === 1) { + this._map[this._player.y][this._player.x] = 0; // Force floor + } + } + console.log("Default game map created. Player position:", this._player.x, this._player.y); + } + + return mapInitialized; + }, + + validatePlayerPosition: function(screenWidth, screenHeight) { + // Validate player position against the map + if (!this._map[this._player.y] || this._map[this._player.y][this._player.x] === undefined) { + console.error(`Player position (${this._player.x}, ${this._player.y}) is invalid or outside map boundaries. Attempting to find fallback.`); + let foundFallback = false; + for (let y_scan = 0; y_scan < screenHeight; y_scan++) { + if (this._map[y_scan]) { + for (let x_scan = 0; x_scan < screenWidth; x_scan++) { + if (this._map[y_scan][x_scan] === 0) { // Find first floor tile + this._player.x = x_scan; + this._player.y = y_scan; + console.warn(`Player moved to fallback position: (${this._player.x}, ${this._player.y})`); + foundFallback = true; + break; + } + } + } + if (foundFallback) break; + } + if (!foundFallback) { + console.error("Could not find a valid fallback starting position for the player on the map."); + return false; + } + } + return true; + }, + + computeFieldOfView: function() { + // Initialize FOV + var fov = new ROT.FOV.PreciseShadowcasting(function(x, y) { + return (this._map[y] && this._map[y][x] !== undefined) ? (this._map[y][x] === 0) : false; + }.bind(this)); + + // If map was NOT built from initialVisibleTiles, compute FOV now + if (!this._visibleTiles || Object.keys(this._visibleTiles).length === 0) { + this._visibleTiles = {}; + fov.compute(this._player.x, this._player.y, 10, function(x, y, r, visibility) { + if (!this._visibleTiles[y]) this._visibleTiles[y] = {}; + if (this._map[y] && this._map[y][x] !== undefined) { + this._visibleTiles[y][x] = this._map[y][x]; + } + }.bind(this)); + } + + this.updateExploredTiles(); + console.log("Initial FOV computed/processed. Visible tiles count:", Object.keys(this._visibleTiles).reduce((acc, yKey) => acc + Object.keys(this._visibleTiles[yKey]).length, 0)); + }, + + updateExploredTiles: function() { + // Add all currently visible tiles to explored tiles + for (var yKey in this._visibleTiles) { + const y = parseInt(yKey); + if (!this._exploredTiles[y]) this._exploredTiles[y] = {}; + for (var xKey in this._visibleTiles[y]) { + const x = parseInt(xKey); + this._exploredTiles[y][x] = this._visibleTiles[y][x]; + } + } + }, + + updateGameState: function(data) { + console.log("🎲 GameState.updateGameState received data:", data); + + if (!this._gameActive) { + console.warn("GameState.updateGameState called, but game is not active. Ignoring update."); + return false; + } + if (!data) { + console.error("GameState.updateGameState: No update data received!"); + return false; + } + + try { + let needsRedraw = false; + + // Update player state + if (data.player) { + this._player = data.player; + needsRedraw = true; + } + + // Update monster state + if (data.monster !== undefined) { + this._monster = data.monster; + needsRedraw = true; + } + + // Update items + if (data.items !== undefined) { + this._items = data.items; + 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) { + console.log("Visible tiles updated. Number of rows:", Object.keys(data.visibleTiles).length); + this._visibleTiles = data.visibleTiles; + + // Debug log for specific problematic coordinates + if (this._visibleTiles[18] && (this._visibleTiles[18][36] !== undefined || this._visibleTiles[18][35] !== undefined)) { + const clientDebug = `🔍 CLIENT y=18: x=35: ${this._visibleTiles[18][35]}, x=36: ${this._visibleTiles[18][36]}`; + console.log(clientDebug); + if (window.GameDebug) window.GameDebug.updateDebugDisplay(clientDebug); + } + if (this._visibleTiles[16] && (this._visibleTiles[16][36] !== undefined || this._visibleTiles[16][35] !== undefined)) { + const clientDebug = `🔍 CLIENT y=16: x=35: ${this._visibleTiles[16][35]}, x=36: ${this._visibleTiles[16][36]}`; + console.log(clientDebug); + if (window.GameDebug) window.GameDebug.updateDebugDisplay(clientDebug); + } + + this.updateExploredTiles(); + needsRedraw = true; + } + + return needsRedraw; + } catch (err) { + console.error("Error in GameState.updateGameState:", err); + return false; + } + }, + + movePlayer: function(dx, dy, screenWidth, screenHeight) { + if (!DisplayManager.ensureDisplay()) return; + + const newX = this._player.x + dx; + const newY = this._player.y + dy; + + // Basic bounds checking + if (newX < 0 || newY < 0 || newX >= screenWidth || newY >= screenHeight) { + console.warn("Attempted to move player outside of bounds:", newX, newY); + return; + } + + // Check if the new position is a wall (1) or out of map bounds + if (this._map[newY] && this._map[newY][newX] !== undefined && this._map[newY][newX] !== 1) { + // Move the player + this._player.x = newX; + this._player.y = newY; + console.log("Player moved to:", this._player.x, this._player.y); + + // Update visibility and redraw + this.updateExploredTiles(); + return true; + } else { + console.log("Move blocked by wall or invalid map data at:", newX, newY); + return false; + } + }, + + getGameStateForRender: function() { + return { + player: this._player, + monster: this._monster, + items: this._items, + entrance: this._entrance, + exit: this._exit, + treasure: this._treasure, + visibleTiles: this._visibleTiles, + exploredTiles: this._exploredTiles, + message: this._message + }; + }, + + // Debug functions + debugPrintMap: function(screenWidth, screenHeight) { + console.log("Current game map:"); + for (let y = 0; y < screenHeight; y++) { + let row = ""; + for (let x = 0; x < screenWidth; x++) { + if (this._map[y] && this._map[y][x] !== undefined) { + row += this._map[y][x] + " "; + } else { + row += "? "; // Unknown/undefined area + } + } + console.log(row); + } + }, + + debugTileMapping: function() { + const testValues = [0, 1, undefined, null]; + for (const val of testValues) { + const char = (val === 1) ? '#' : "'"; + const debugMsg = `Test mapping: value=${val} -> char='${char}'`; + console.log(debugMsg); + if (window.GameDebug) window.GameDebug.updateDebugDisplay(debugMsg); + } + }, + + // Compute field of view for the player (LOCAL TESTING ONLY - server handles FOV) + computeFieldOfView: function() { + if (!this._player) { + console.warn("Cannot compute FOV: player not initialized"); + return; + } + + console.log("⚠️ LOCAL FOV COMPUTATION (should only be used for testing without server)"); + + const oldVisibleTiles = JSON.parse(JSON.stringify(this._visibleTiles)); + this._visibleTiles = {}; + + const fov = new ROT.FOV.PreciseShadowcasting(function(x, y) { + return this._map[y] && this._map[y][x] === 0; // 0 = passable, 1 = wall + }.bind(this)); + + fov.compute(this._player.x, this._player.y, 6, function(x, y, r, visibility) { + if (!this._visibleTiles[y]) this._visibleTiles[y] = {}; + this._visibleTiles[y][x] = this._map[y] && this._map[y][x] !== undefined ? this._map[y][x] : 0; + + // Mark as explored + if (!this._exploredTiles[y]) this._exploredTiles[y] = {}; + this._exploredTiles[y][x] = this._map[y] && this._map[y][x] !== undefined ? this._map[y][x] : 0; + }.bind(this)); + + console.log("FOV computed for player at:", this._player.x, this._player.y); + console.log("Visible tiles:", Object.keys(this._visibleTiles).length); + }, + + // Update game state with new data from server + updateGameState: function(data) { + let needsRedraw = false; + + if (data.player && (data.player.x !== this._player.x || data.player.y !== this._player.y)) { + this._player = data.player; + needsRedraw = true; + } + + if (data.monster) { + this._monster = data.monster; + needsRedraw = true; + } + + if (data.items) { + this._items = data.items; + needsRedraw = true; + } + + if (data.map) { + this._map = data.map; + needsRedraw = true; + } + + // Handle visibleTiles from server (this is the FOV data) + if (data.visibleTiles) { + this._visibleTiles = data.visibleTiles; + + // Update explored tiles with newly visible areas + for (const yKey in this._visibleTiles) { + const y = parseInt(yKey); + if (!this._exploredTiles[y]) this._exploredTiles[y] = {}; + for (const xKey in this._visibleTiles[y]) { + const x = parseInt(xKey); + this._exploredTiles[y][x] = this._visibleTiles[y][x]; + } + } + + // console.log("Updated visibleTiles from server, visible tile count:", + // Object.keys(this._visibleTiles).reduce((count, y) => count + Object.keys(this._visibleTiles[y]).length, 0)); + needsRedraw = true; + } + + if (data.entrance) { + this._entrance = data.entrance; + needsRedraw = true; + } + + if (data.exit) { + this._exit = data.exit; + needsRedraw = true; + } + + if (data.treasure) { + this._treasure = data.treasure; + needsRedraw = true; + } + + if (data.message) { + this._message = data.message; + needsRedraw = true; + } + + return needsRedraw; + }, + + // Get game state data for rendering + getGameStateForRender: function() { + return { + map: this._map, + player: this._player, + monster: this._monster, + items: this._items, + entrance: this._entrance, + exit: this._exit, + treasure: this._treasure, + visibleTiles: this._visibleTiles, + exploredTiles: this._exploredTiles, + message: this._message, + gameActive: this._gameActive + }; + }, + + // Move player and update game state + movePlayer: function(dx, dy, screenWidth, screenHeight) { + if (!this._player) { + console.warn("Cannot move player: player not initialized"); + return false; + } + + const newX = this._player.x + dx; + const newY = this._player.y + dy; + + // Check bounds + if (newX < 0 || newX >= screenWidth || newY < 0 || newY >= screenHeight) { + // console.log("Player move blocked: out of bounds"); + return false; + } + + // Check if the new position is passable + if (this._map[newY] && this._map[newY][newX] === 1) { + // console.log("Player move blocked: wall at", newX, newY); + return false; + } + + // Update player position + this._player.x = newX; + this._player.y = newY; + + // Recompute field of view + this.computeFieldOfView(); + + // console.log("Player moved to:", this._player.x, this._player.y); + return true; + } +}; + +// Make it available globally +if (typeof window !== 'undefined') { + window.GameState = GameState; +} diff --git a/html/js/inputHandler.js b/html/js/inputHandler.js index bbeeaf4..0326488 100644 --- a/html/js/inputHandler.js +++ b/html/js/inputHandler.js @@ -30,19 +30,34 @@ const InputHandler = { 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 the message is "enter", always send it to server + // The server will handle queueing logic and timing if (msg.toLowerCase() === 'enter') { - console.log("🔑 'ENTER' COMMAND DETECTED - expecting game_start response"); + console.log("🔑 'ENTER' COMMAND DETECTED - sending to server"); + socket.emit('chat message', msg); - // Show waiting screen - if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { - Game.drawWaitingScreen(); + // Show appropriate screen based on current state + const isDebugMode = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.protocol === 'file:'; + + if (isDebugMode || ScreenManager.canEnterGame()) { + console.log("✅ Can enter immediately - showing waiting screen"); + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { + Game.drawWaitingScreen(); + } + } else { + console.log("⏳ Will be queued - showing queue message"); + $('#messages').append($('
    • ').text("* You have been added to the queue! Game will start after next block.")); + UI.scrollChat(); } + } else { + // Send other chat messages normally + socket.emit('chat message', msg); } + $('#chatInput').val(''); return false; }); @@ -104,7 +119,35 @@ const InputHandler = { } } } else { - console.log("Game display has focus, but Game is not active."); + // Game display has focus but game is not active (welcome screen) + // Check for debug key 'D' to start immediately + if (e.key === 'D' || e.key === 'd') { + // Check if we're in debug mode (localhost) + const isDebugMode = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.protocol === 'file:'; + + if (isDebugMode) { + console.log("🔧 DEBUG: 'D' key pressed - starting game immediately"); + e.preventDefault(); + + // Emit enter command to start game immediately + socket.emit('chat message', 'enter'); + + // Show waiting screen while game starts + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { + Game.drawWaitingScreen(); + } + + // Add debug message to chat + $('#messages').append($('
    • ').text("🔧 DEBUG: Starting game immediately...")); + UI.scrollChat(); + } else { + console.log("Debug key pressed but not in debug mode"); + } + } 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 diff --git a/html/js/options.js b/html/js/options.js index 180f645..229a2ee 100644 --- a/html/js/options.js +++ b/html/js/options.js @@ -1,33 +1,15 @@ var tileSet = document.createElement("img"); var tileSetLoaded = false; -// More robust error handling tileSet.onload = function() { - console.log("✅ Tileset image loaded successfully!"); tileSetLoaded = true; - - // Add visible verification on the page - var statusElem = document.createElement('div'); - statusElem.style.position = 'fixed'; - statusElem.style.bottom = '40px'; - statusElem.style.right = '10px'; - statusElem.style.background = 'rgba(0,255,0,0.5)'; - statusElem.style.color = 'white'; - statusElem.style.padding = '5px'; - statusElem.style.fontSize = '12px'; - statusElem.style.zIndex = '1000'; - statusElem.textContent = '✓ Tileset loaded'; - document.body.appendChild(statusElem); - setTimeout(() => statusElem.remove(), 5000); }; tileSet.onerror = function() { - console.error("❌ Failed to load tileset image!"); - window.globalTileSetStatus = "error"; // Set a status for Game.js to check if needed + console.error("Failed to load tileset image!"); }; -// Try different possible paths (to handle serving from different directories) -tileSet.src = "tiles.png"; // Try default path first +tileSet.src = "tiles.png"; var options = { layout: "tile", @@ -130,17 +112,12 @@ var options = { "]": [832, 64], "{": [864, 64], "%": [896, 64], - "^": [928, 64], // Duplicate ^ + "^": [928, 64], " ": [992, 64] } }; -// Update the tileset loading to properly set the global variables - -// Make sure the variables are properly set in the global scope +// Make options globally available window.tileSet = tileSet; window.tileMap = options.tileMap; - -// Add logging to confirm they're set -console.log("✅ Tileset assigned to window.tileSet:", !!window.tileSet); -console.log("✅ TileMap assigned to window.tileMap:", !!window.tileMap, "with keys:", window.tileMap ? Object.keys(window.tileMap).length : 0); +window.options = options; diff --git a/html/js/renderEngine.js b/html/js/renderEngine.js new file mode 100644 index 0000000..ecf39dd --- /dev/null +++ b/html/js/renderEngine.js @@ -0,0 +1,171 @@ +// RenderEngine - Handles game screen rendering and drawing logic +var RenderEngine = { + defaultFg: "#FFF", + + init: function(screenWidth, screenHeight) { + this._screenWidth = screenWidth; + this._screenHeight = screenHeight; + }, + + isVisible: function(x, y, visibleTiles) { + return visibleTiles[y] && visibleTiles[y][x] !== undefined; + }, + + drawGameScreen: function(gameState) { + if (!DisplayManager.ensureDisplay()) return; + + const display = DisplayManager.getDisplay(); + const player = gameState.player; + const monster = gameState.monster; + const items = gameState.items; + const entrance = gameState.entrance; + const exit = gameState.exit; + const treasure = gameState.treasure; + const visibleTiles = gameState.visibleTiles; + const exploredTiles = gameState.exploredTiles; + const message = gameState.message; + + const playerWX = player.x; + const playerWY = player.y; + const centerX = Math.floor(this._screenWidth / 2); + const centerY = Math.floor(this._screenHeight / 2); + + const topLeftWX = playerWX - centerX; + const topLeftWY = playerWY - centerY; + + for (let sy = 0; sy < this._screenHeight; sy++) { + for (let sx = 0; sx < this._screenWidth; sx++) { + const wx = topLeftWX + sx; + const wy = topLeftWY + sy; + + let charStack = []; + let fgStack = []; + let bgStack = []; + + let baseChar = ' '; + let baseFg = this.defaultFg; + let tileType; + + // Render base terrain + if (this.isVisible(wx, wy, visibleTiles)) { + tileType = visibleTiles[wy][wx]; + baseFg = this.defaultFg; + + // Fix: Handle all possible tile values explicitly and ensure they map to valid characters + if (tileType === 1) { + baseChar = '#'; // Wall + } else if (tileType === 0) { + baseChar = "'"; // Floor + } else { + baseChar = "'"; // Default to floor for safety + tileType = 0; + } + } else if (exploredTiles[wy] && exploredTiles[wy][wx] !== undefined) { + tileType = exploredTiles[wy][wx]; + baseFg = this.defaultFg; + + // Fix: Handle all possible tile values explicitly for explored tiles too + if (tileType === 1) { + baseChar = '#'; // Wall + } else if (tileType === 0) { + baseChar = "'"; // Floor + } else { + baseChar = "'"; // Default to floor for safety + tileType = 0; + } + } else { + // No visible or explored tile - render empty space + baseChar = ' '; + baseFg = this.defaultFg; + } + + // Only add to stacks if we have a valid character + if (baseChar && baseChar !== ' ') { + charStack.push(baseChar); + fgStack.push("transparent"); // Use transparent for all base tiles + bgStack.push("transparent"); // Use transparent for all base tiles + } + + // Render items + if (items) { + for (const itemKey in items) { + if (items.hasOwnProperty(itemKey)) { + const currentItem = items[itemKey]; + if (currentItem && currentItem.x === wx && currentItem.y === wy && this.isVisible(wx, wy, visibleTiles)) { + charStack.push('$'); + fgStack.push("transparent"); // Use transparent for now + bgStack.push("transparent"); + } + } + } + } + + // Render entrance + if (entrance && entrance[0] === wx && entrance[1] === wy && this.isVisible(wx, wy, visibleTiles)) { + charStack.push('<'); + fgStack.push("transparent"); // Use transparent for now + bgStack.push("transparent"); + } + + // Render exit + if (exit && exit[0] === wx && exit[1] === wy && this.isVisible(wx, wy, visibleTiles)) { + charStack.push('>'); + fgStack.push("transparent"); // Use transparent for now + bgStack.push("transparent"); + } + + // Render treasure + if (treasure && treasure[0] === wx && treasure[1] === wy && this.isVisible(wx, wy, visibleTiles)) { + charStack.push('$'); + fgStack.push("transparent"); // Use transparent for now + bgStack.push("transparent"); + } + + // Render monster + if (monster && monster.x === wx && monster.y === wy && this.isVisible(wx, wy, visibleTiles)) { + charStack.push('~'); + fgStack.push("transparent"); // Use transparent for now + bgStack.push("transparent"); + } + + // Render player (always on top) + if (wx === playerWX && wy === playerWY) { + charStack.push('@'); + fgStack.push("transparent"); // Use transparent for now + bgStack.push("transparent"); + } + + if (charStack.length > 0) { + // Validate array lengths + while (fgStack.length < charStack.length) fgStack.push("transparent"); + while (bgStack.length < charStack.length) bgStack.push("transparent"); + + // Validate characters are in tileMap + for (let i = 0; i < charStack.length; i++) { + if (!charStack[i] || (window.options?.tileMap && !window.options.tileMap.hasOwnProperty(charStack[i]))) { + charStack[i] = "'"; + } + if (!fgStack[i]) fgStack[i] = "transparent"; + if (!bgStack[i]) bgStack[i] = "transparent"; + } + + try { + display.draw(sx, sy, charStack, fgStack, bgStack); + } catch (error) { + display.draw(sx, sy, ["'"], ["transparent"], ["transparent"]); + } + } + } + } + + // Render message at bottom if present + if (message) { + ScreenManager.drawCenteredText(this._screenHeight - 1, message); + } + } +}; + +// Make it available globally +if (typeof window !== 'undefined') { + window.RenderEngine = RenderEngine; +} diff --git a/html/js/screenManager.js b/html/js/screenManager.js new file mode 100644 index 0000000..da45623 --- /dev/null +++ b/html/js/screenManager.js @@ -0,0 +1,181 @@ +// ScreenManager - Handles different screen states and UI drawing +var ScreenManager = { + defaultFg: "#FFF", + _currentBlockHeight: 0, + _blockTimer: null, + _waitingForBlock: false, + + init: function(screenWidth, screenHeight) { + this._screenWidth = screenWidth; + this._screenHeight = screenHeight; + this._currentBlockHeight = 0; + this._waitingForBlock = false; + }, + + drawCenteredText: function(y, text, color) { + if (!DisplayManager.ensureDisplay()) return y; + const display = DisplayManager.getDisplay(); + + // Filter text to only include valid characters + let filteredText = ""; + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (window.options?.tileMap?.hasOwnProperty(char) || char === ' ') { + filteredText += char; + } else { + filteredText += "?"; + } + } + + const x = Math.floor((this._screenWidth - filteredText.length) / 2); + const finalColor = color || this.defaultFg; + display.drawText(x, y, `%c{${finalColor}}${filteredText}`); + return y + 1; + }, + + drawBorder: function() { + if (!DisplayManager.ensureDisplay()) return; + const display = DisplayManager.getDisplay(); + + for (var i = 0; i < this._screenWidth; i++) { + display.draw(i, 0, "-", this.defaultFg, null); + display.draw(i, this._screenHeight - 1, "-", this.defaultFg, null); + } + for (var j = 1; j < this._screenHeight - 1; j++) { + display.draw(0, j, "|", this.defaultFg, null); + display.draw(this._screenWidth - 1, j, "|", this.defaultFg, null); + } + }, + + drawWelcomeScreen: function() { + if (!DisplayManager.ensureDisplay()) return; + DisplayManager.clearDisplay(); + + let y = Math.floor(this._screenHeight / 4); + y = this.drawCenteredText(y, "THE WOWNGEON", this.defaultFg); + y = this.drawCenteredText(y + 2, "A Wownero Roguelike", this.defaultFg); + + y = this.drawCenteredText(y + 3, "Waiting for next block...", this.defaultFg); + y = this.drawCenteredText(y + 1, `Block Height: ${this._currentBlockHeight}`, "#0f0"); + + if (this._waitingForBlock) { + y = this.drawCenteredText(y + 2, "Mining in progress...", "#ff0"); + } else { + y = this.drawCenteredText(y + 2, "Type 'ENTER' to queue for game", this.defaultFg); + } + + // Debug button (shown only in development) + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:') { + this.drawCenteredText(y + 3, "[DEBUG: Press 'D' to start immediately]", "#666"); + } + + this.drawCenteredText(y + 5, "Good luck, adventurer!", this.defaultFg); + }, + + // Start simulated block mining (30 second intervals) + startBlockSimulation: function() { + console.log("Starting block simulation..."); + this._currentBlockHeight = Math.floor(Math.random() * 1000000) + 3200000; // Realistic Wownero block height + this._waitingForBlock = true; + + // Update welcome screen every second during mining + const updateInterval = setInterval(() => { + if (this._waitingForBlock) { + this.drawWelcomeScreen(); + } else { + clearInterval(updateInterval); + } + }, 1000); + + // Simulate new block every 30 seconds + this._blockTimer = setInterval(() => { + this._currentBlockHeight++; + this._waitingForBlock = false; + this.drawWelcomeScreen(); + + // Show block found message briefly + setTimeout(() => { + if (!DisplayManager.ensureDisplay()) return; + let y = Math.floor(this._screenHeight / 2); + this.drawCenteredText(y, "🎉 NEW BLOCK FOUND! 🎉", "#0f0"); + this.drawCenteredText(y + 1, "You may now enter the dungeon!", "#0f0"); + + // Allow entry for 5 seconds + setTimeout(() => { + this._waitingForBlock = true; + this.drawWelcomeScreen(); + }, 5000); + }, 500); + + }, 30000); // 30 second intervals + }, + + // Stop block simulation + stopBlockSimulation: function() { + if (this._blockTimer) { + clearInterval(this._blockTimer); + this._blockTimer = null; + } + this._waitingForBlock = false; + }, + + // Check if player can enter (not waiting for block or debug mode) + canEnterGame: function() { + const isDebugMode = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.protocol === 'file:'; + return !this._waitingForBlock || isDebugMode; + }, + + drawWinScreen: function(hasTreasure) { + if (!DisplayManager.ensureDisplay()) return; + DisplayManager.clearDisplay(); + + let y = this.drawCenteredText(Math.floor(this._screenHeight / 3), "CONGRATULATIONS!"); + if (hasTreasure) { + y = this.drawCenteredText(y + 2, "You escaped with the treasure!"); + } else { + y = this.drawCenteredText(y + 2, "You escaped the dungeon!"); + } + this.drawCenteredText(y + 3, "A true hero of Wownero!"); + }, + + drawLoseScreen: function(reason) { + if (!DisplayManager.ensureDisplay()) return; + DisplayManager.clearDisplay(); + + let y = this.drawCenteredText(Math.floor(this._screenHeight / 3), "YOU HAVE PERISHED"); + if (reason === 'monster') { + y = this.drawCenteredText(y + 2, "Slain by a fearsome beast."); + } else if (reason === 'timeout') { + y = this.drawCenteredText(y + 2, "The dungeon claimed you before the next block."); + } else { + y = this.drawCenteredText(y + 2, "Your adventure ends here."); + } + this.drawCenteredText(y + 3, "Better luck next time."); + }, + + drawWaitingScreen: function() { + if (!DisplayManager.ensureDisplay()) return; + DisplayManager.clearDisplay(); + + let y = Math.floor(this._screenHeight / 2) - 2; + y = this.drawCenteredText(y, "* MINING IN PROGRESS *", "#ff0"); + y = this.drawCenteredText(y + 2, `Current Block: ${this._currentBlockHeight}`, "#0f0"); + y = this.drawCenteredText(y + 1, "Waiting for next block to enter...", this.defaultFg); + + // Animated dots + const dots = ".".repeat((Math.floor(Date.now() / 500) % 4)); + this.drawCenteredText(y + 2, `Mining${dots}`, "#666"); + }, + + stopWaitingScreen: function() { + if (!DisplayManager.ensureDisplay()) return; + DisplayManager.clearDisplay(); + } +}; + +// Make it available globally +if (typeof window !== 'undefined') { + window.ScreenManager = ScreenManager; +} diff --git a/html/js/socketHandlers.js b/html/js/socketHandlers.js index 4efc7d8..4f330d5 100644 --- a/html/js/socketHandlers.js +++ b/html/js/socketHandlers.js @@ -46,11 +46,11 @@ const SocketHandlers = { userAgent: navigator.userAgent }); - // Auto-start game for faster testing + // Auto-start for testing (timeout issue has been fixed) setTimeout(() => { console.log("🚀 AUTO-STARTING GAME for faster testing"); socket.emit('chat message', 'enter'); - }, 500); // Small delay to ensure connection is fully established + }, 500); }, onWelcome: function(msg) { diff --git a/html/test.html b/html/test.html deleted file mode 100644 index ad1f55a..0000000 --- a/html/test.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - Wowgue - - - - - - -
      -

      Welcome traveler...

      -
      -
      -
      1
      -
      2
      -
      3
      -
      4
      -
      5
      -
      6sssssssssssssssssssssssss
      -
      - - - \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..4e32d75 --- /dev/null +++ b/index.js @@ -0,0 +1,422 @@ +const express = require('express'); +var app = require('express')(); +var http = require('http').Server(app); +var io = require('socket.io')(http); +var rpc = require('./rpccalls.js'); +var db = require('./dbcalls.js'); +var user = require('./user'); + +// Function to get a user by socket ID +// Enhance the getUserBySocket function to try both IDs + +function getUserBySocket(socketId) { + console.log(`Looking up user with socketId: ${socketId}`); + + // Try direct lookup first + let foundUser = user.getUserBySocketId(socketId); + + // If not found but we have a socket map, try the mapped ID + if (!foundUser && clientSocketMap.has(socketId)) { + const mappedId = clientSocketMap.get(socketId); + console.log(`Socket ID ${socketId} not found directly, trying mapped ID: ${mappedId}`); + foundUser = user.getUserBySocketId(mappedId); + } + + console.log(`User lookup result for ${socketId}: ${foundUser ? "FOUND" : "NOT FOUND"}`); + return foundUser; +} + +const Game = require('./game'); +const activeGames = new Map(); // Maps socketId to Game objects +let lastBlockHeight = 0; // Track last block height +const MIN_FEE = 10; // Minimum entrance fee in WOW +const MAX_FEE = 100; // Maximum entrance fee in WOW +const WAITING_PLAYERS = []; // Players waiting for the next block +const DEBUG_MODE = true; // Set to false to disable debug mode +let debugBlockHeight = 1; +const clientSocketMap = new Map(); // To track client-to-server socket ID mappings + +app.use(express.static('html')); +app.get('/', function(req, res) { + res.sendfile('index.html'); +}); + +// Replace your existing block check interval with this conditional version +if (DEBUG_MODE) { + console.log("🐛 DEBUG MODE ENABLED - Simulating blocks every 30 seconds"); + + // Initial debug block + io.emit('blockheight', debugBlockHeight); + + // Debug block height simulator + setInterval(function() { + debugBlockHeight++; + console.log(`🐛 DEBUG: New simulated block: ${debugBlockHeight}`); + io.emit('blockheight', debugBlockHeight); + + // Start games for waiting players + startGamesForWaiting(debugBlockHeight); + + // Check active games for timeout + checkGamesTimeout(debugBlockHeight); + + }, 30000); // Every 30 seconds +} else { + // Your original block check code + setInterval(function() { + rpc.daemonCall("get_block_count", "", function(result) { + const currentHeight = result.result.count; + rpc.lastBlock.setHeight(currentHeight); + + // If new block found + if (currentHeight > lastBlockHeight) { + console.log(`New block found: ${currentHeight}`); + io.emit('blockheight', currentHeight); + + // Start games for waiting players + startGamesForWaiting(currentHeight); + + // Check active games for timeout + checkGamesTimeout(currentHeight); + + lastBlockHeight = currentHeight; + } else { + io.emit('blockheight', currentHeight); + } + }); + }, 5000); +} + +// Start games for waiting players +function startGamesForWaiting(blockHeight) { + console.log(`Starting games for ${WAITING_PLAYERS.length} waiting players at block height ${blockHeight}`); + + while (WAITING_PLAYERS.length > 0) { + const playerEntry = WAITING_PLAYERS.shift(); + const serverId = playerEntry.serverId; + + console.log(`Processing player: server=${serverId}`); + + const currentUser = getUserBySocket(serverId); + + if (currentUser) { + currentUser.blockRec = blockHeight; + + try { + const game = currentUser.startGame(80, 40); + activeGames.set(serverId, game); + + const gameState = game.getState(); + gameState.blockHeight = blockHeight; + + console.log(`🎮 SENDING GAME_START to ${serverId}`); + + // Send ONLY ONE message - the game state itself + io.to(serverId).emit('game_start', gameState); + + console.log(`Game started for player ${serverId}`); + } catch (error) { + console.error(`Error creating game:`, error); + io.to(serverId).emit('message', 'Error starting game: ' + error.message); + } + } else { + console.error(`User not found for socket ${serverId}`); + io.to(serverId).emit('message', 'Error: User not found'); + } + } +} + +// Check if any active games have timed out +function checkGamesTimeout(currentHeight) { + activeGames.forEach((game, socketId) => { + const user = getUserBySocket(socketId); + + // Player has 3 blocks to complete the dungeon + // If they started on block 2, they must escape before block 5 is found + if (user && user.blockRec && currentHeight > user.blockRec + 2) { + console.log(`Game timeout for player ${socketId}: started on block ${user.blockRec}, current block ${currentHeight} (allowed until block ${user.blockRec + 2})`); + + // Game has timed out - player didn't escape in time + game.gameState = 'lost'; + io.to(socketId).emit('game_over', { + status: 'lost', + reason: 'timeout', + message: 'You didn\'t escape before the block time limit!' + }); + + activeGames.delete(socketId); + } + }); +} + +// Socket.io handlers +io.on('connection', function(socket) { + console.log('A user connected'); + console.log(socket.client.id); + console.log(socket.handshake.address); + + // Create and register user (automatically stored in registry) + new user.User(socket.id, socket.handshake.address); + // Also store socket.client.id as an attribute + const newUser = getUserBySocket(socket.id); + if (newUser) { + newUser.clientId = socket.client.id; + console.log(`User created with both socket.id (${socket.id}) and socket.client.id (${socket.client.id})`); + } + + // Insert into visitors DB + db.insertVisitor(socket.client.id, socket.handshake.address, 0); + + // Send welcome message + io.to(socket.client.id).emit('welcome', socket.client.id); + + socket.on('chat message', function(msg) { + console.log('Message received:', msg); + + if (msg.toLowerCase() == 'hello') { + io.to(`${socket.client.id}`).emit('message', + 'Welcome, to enter the dungeon type Enter, and you will be given a Wownero address, ' + + 'which you must send between 10-100 WOW to enter. The more you send, the more you can win.'); + } + // Update the 'enter' command handler to send waiting_status + if (msg.toLowerCase() == 'enter') { + console.log(`Player ${socket.id} requested to enter the dungeon`); + + // Use the updated user lookup function + const currentUser = getUserBySocket(socket.id); + + if (currentUser) { + console.log(`Found user for socket ${socket.id}, adding to queue...`); + + // Add to waiting players queue + if (!WAITING_PLAYERS.some(p => p.serverId === socket.id)) { + WAITING_PLAYERS.push({ + serverId: socket.id, + clientId: socket.id, + user: currentUser, + joinedAt: Date.now() + }); + + // Send waiting status data for display - THIS IS THE KEY PART + io.to(socket.id).emit('waiting_status', { + status: 'waiting', + message: 'Waiting for the next block to be found...', + position: WAITING_PLAYERS.length, + currentBlock: debugBlockHeight || lastBlockHeight || 0, + joinTime: Date.now() + }); + + console.log(`Added player to queue. Current queue: ${WAITING_PLAYERS.length} players`); + } else { + io.to(socket.id).emit('message', 'You are already in the queue. Please wait.'); + } + } else { + io.to(socket.id).emit('message', 'Error: Could not add you to the game queue. Please try again.'); + } + } + // Add a handler for the 'cancel' command + else if (msg.toLowerCase() == 'cancel') { + // Remove player from waiting queue + const index = WAITING_PLAYERS.findIndex(p => p.serverId === socket.id); + + if (index !== -1) { + WAITING_PLAYERS.splice(index, 1); + io.to(socket.id).emit('message', 'You have left the queue.'); + + // Reset to welcome screen + io.to(socket.id).emit('queue_cancelled'); + } else { + io.to(socket.id).emit('message', 'You were not in the queue.'); + } + } + else { + io.emit('message', msg); + } + }); + + // Handle player movement + socket.on('player_move', function(moveData) { // Changed from 'move' to 'player_move' and added moveData + console.log(`Player move event received from ${socket.id}:`, moveData); + const currentUser = getUserBySocket(socket.id); + + if (currentUser && currentUser.game && currentUser.game.gameState === 'active') { + if (typeof moveData.dx === 'number' && typeof moveData.dy === 'number') { + const game = currentUser.game; + const moveResult = game.movePlayer(moveData.dx, moveData.dy); // This should update player pos and FOV + + if (moveResult && moveResult.status === 'moved') { + // 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 + + // Log before sending to client for debugging + console.log(`Sending game_update to ${socket.id} after player move. Player:`, updatedGameState.player, "Visible tiles keys:", Object.keys(updatedGameState.visibleTiles || {})); + + io.to(socket.id).emit('game_update', updatedGameState); + } else { + console.log(`Player move from ${socket.id} was invalid or resulted in no change.`); + // Optionally, send an update even for invalid moves if you want to provide feedback + // const currentGameState = game.getState(); + // io.to(socket.id).emit('game_update', currentGameState); + } + } else { + console.error(`Invalid moveData received from ${socket.id}:`, moveData); + } + } else { + console.log(`Player move event from ${socket.id} ignored: No active game found for user.`); + } + }); + + // Handle disconnection + socket.on('disconnect', function() { + console.log('User disconnected', socket.client.id); + user.removeUser(socket.client.id); + }); + + // Add this to your socket handlers in index.js + socket.on('debug_ping', function(data) { + console.log(`Debug ping received from ${socket.client.id}`); + socket.emit('debug_pong', { + message: "Hello from server!", + clientTime: data.time, + serverTime: Date.now(), + socketId: socket.client.id + }); + }); + + socket.on('register_client', function(data) { + console.log(`Client registered: ${socket.id} (server) <-> ${data.clientId} (client)`); + + // Store both mappings for lookup + clientSocketMap.set(data.clientId, socket.id); + clientSocketMap.set(socket.id, data.clientId); + + // Send confirmation back to client + socket.emit('socket_registered', { + clientId: data.clientId, + serverId: socket.id, + success: true + }); +}); + +}); // End of io.on('connection') handler + +// Add this debug function to check all registered users + +function debugRegisteredUsers() { + console.log("\n🔍 CHECKING ALL REGISTERED USERS:"); + const allUsers = user.getAllUsers ? user.getAllUsers() : null; + + if (!allUsers) { + console.log("No getAllUsers function available or no users registered"); + return; + } + + console.log(`Found ${allUsers.length} registered users:`); + allUsers.forEach((u, i) => { + console.log(`User ${i+1}:`, { + id: u.id, + clientId: u.clientId || 'undefined', + address: u.address + }); + }); + console.log(); // Empty line for readability +} + +// Call this after a user registers and at intervals +setInterval(debugRegisteredUsers, 10000); + +http.listen(3000, function() { + console.log('listening on *:3000'); +}); + +// Make sure this function is defined in your file +function startGameForPlayer(socketId, blockHeight) { + console.log(`Starting game for player ${socketId} at block height ${blockHeight}`); + + const currentUser = getUserBySocket(socketId); + if (!currentUser) { + console.error(`User with socket ID ${socketId} not found`); + return; + } + + try { + // Create a new game + const game = currentUser.startGame(25, 19); + if (!game) { + console.error("Game creation failed"); + return; + } + + // Store the game in activeGames map + activeGames.set(socketId, game); + + // Get game state - ensure it has the required fields + const gameState = { + gameState: "active", // Add this so frontend knows it's active + player: { + x: game.player.x, + y: game.player.y, + hasKey: !!game.player.hasKey, + hasTreasure: !!game.player.hasTreasure + }, + monster: game.monster, + visibleTiles: game.calculateFOV ? game.calculateFOV() : {}, + entrance: [game.dungeon.entrance.x, game.dungeon.entrance.y], + exit: [game.dungeon.exit.x, game.dungeon.exit.y], + treasure: [game.dungeon.treasure.x, game.dungeon.treasure.y] + }; + + console.log("Sending game_start with state:", JSON.stringify(gameState, null, 2).substring(0, 200) + "..."); + + // Add a small delay before sending (sometimes helps with socket timing issues) + setTimeout(() => { + debugSocket(socketId, 'game_start', gameState); + // Send initial game state to client + io.to(socketId).emit('game_start', gameState); + console.log(`Game started for player ${socketId}`); + }, 100); + } catch (err) { + console.error("Error starting game:", err); + io.to(socketId).emit('message', 'Error starting game: ' + err.message); + } +} + +function debugSocket(socketId, eventName, data) { + console.log(`🔌 SOCKET DEBUG: Sending ${eventName} to ${socketId.substring(0, 8)}...`); + console.log(`📦 PAYLOAD (first 300 chars): ${JSON.stringify(data).substring(0, 300)}...`); +} diff --git a/src/backend/index.js b/src/backend/index.js index be8741f..84de748 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -133,17 +133,17 @@ function checkGamesTimeout(currentHeight) { activeGames.forEach((game, socketId) => { const user = getUserBySocket(socketId); - // 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}`); + // Player has 3 blocks to complete the dungeon + // If they started on block 2, they must escape before block 5 is found + if (user && user.blockRec && currentHeight > user.blockRec + 2) { + console.log(`Game timeout for player ${socketId}: started on block ${user.blockRec}, current block ${currentHeight} (allowed until block ${user.blockRec + 2})`); // Game has timed out - player didn't escape in time game.gameState = 'lost'; io.to(socketId).emit('game_over', { status: 'lost', reason: 'timeout', - message: 'You didn\'t escape before the next block was found!' + message: 'You didn\'t escape before the block time limit!' }); activeGames.delete(socketId);