diff --git a/README.md b/README.md index 40d7317..c77e335 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ The game is built using Node.js for the backend, with Express and Socket.io for - **Real-time multiplayer**: Multiple players can play simultaneously with efficient socket broadcasting - **Structured chat system**: Public chat broadcasts and private status updates with command separation - **Field of view**: Limited visibility adds challenge and atmosphere -- **HTML START button**: Visible button in status area for game entry -- **Auto-return system**: 30-second timeout returns players to title screen after game over - **Spectator-ready architecture**: Socket system designed for future spectator mode implementation ## Technology Stack @@ -52,24 +50,31 @@ The game implements a sophisticated real-time communication system using Socket. ## Architecture -The project has been refactored into a clean, modular architecture with clear separation of concerns: +The project uses a clean, modular architecture with clear separation of concerns: ### Backend Structure (`src/backend/`) -- **index.js** - Main server file handling HTTP requests, WebSocket connections, game lifecycle, blockchain monitoring, and structured socket event broadcasting -- **game.js** - Game class with dungeon generation, player movement, monster AI, and field-of-view calculations -- **user.js** - User registration, authentication, game association, and payment tracking +The backend has been refactored into focused modules for maintainability and scalability: + +- **index.js** - Main server orchestrator (69 lines) coordinating all modules +- **socketHandlers.js** - Complete socket event processing system (400+ lines) +- **broadcastManager.js** - Centralized communication and broadcasting management +- **debugManager.js** - Debug mode and development utilities with production separation +- **game.js** - Game class with factory methods and proper configuration integration +- **user.js** - User management class with statistics tracking +- **dungeon.js** - Dungeon generation with centralized configuration system +- **lightingAndFov.js** - Field of view calculations and lighting system - **player.js** - Player state management and movement logic - **monster.js** - Monster AI and behavior -- **dungeon.js** - Dungeon generation and map management - **dbcalls.js** - Database operations for user data and game history - **rpccalls.js** - Wownero blockchain RPC integration -#### Socket Event Broadcasting (index.js) -- **Helper Functions**: `sendGameUpdate()`, `broadcastBlockHeight()`, `sendStatusUpdate()` -- **Event Types**: Global broadcasts, player-specific updates, status messages -- **Spectator Ready**: Architecture supports future multi-cast to spectators -- **Regular Broadcasting**: Block height sent every 5 seconds to all clients +#### Modular Architecture Benefits +- **Single Responsibility**: Each module has one clear purpose +- **Dependency Injection**: Modules receive dependencies cleanly +- **Future-Ready**: Easy to add features like spectator mode +- **Debug/Production Split**: Development and production logic properly separated +- **Maintainable**: 600-line monolith reduced to focused 69-line orchestrator ### Frontend Structure (`html/`) @@ -81,10 +86,9 @@ The project has been refactored into a clean, modular architecture with clear se - Tile-based rendering system 2. **ScreenManager.js** - Screen State Management - - Welcome screen with block simulation (30-second intervals) + - Welcome screen with block simulation - Win/lose screens and waiting screens - Centered text drawing utilities - - HTML START button visibility control 3. **RenderEngine.js** - Game Rendering Engine - Game screen rendering and object drawing @@ -94,8 +98,7 @@ The project has been refactored into a clean, modular architecture with clear se 4. **GameState.js** - Game State Management - Player, map, monster, item data management - Game state updates and validation - - Field of view computation fixes - - Player movement logic + - Field of view computation and player movement logic 5. **game.js** - Main Game Controller - Coordinates all modules with clean APIs @@ -103,39 +106,35 @@ The project has been refactored into a clean, modular architecture with clear se - Handles game initialization and flow 6. **Additional Modules**: - - **inputHandler.js** - Keyboard/mouse input with movement throttling and HTML button handling + - **inputHandler.js** - Keyboard/mouse input with movement throttling - **socketHandlers.js** - WebSocket communication with initialization protection - **ui.js** - User interface management and chat functionality ## Socket Event Architecture -The game uses a structured socket.io event system designed for real-time multiplayer gameplay with future spectator support: +The game uses a sophisticated socket.io event system designed for real-time multiplayer gameplay with future spectator support: ### Event Broadcasting Strategy #### **Block Height Broadcasting** (All Clients) -- `blockheight` - Broadcast to ALL connected clients every 5 seconds +- `blockheight` - Broadcast to ALL connected clients regularly - Ensures all players stay synchronized with blockchain state - Immediate emission on client connection -- Regular updates prevent clients from missing block changes #### **Game State Updates** (Player + Future Spectators) - `game_update` - Currently sent to active player only - **Future Goal**: Broadcast to spectators watching specific games -- Structured for easy expansion to multi-viewer support - Contains player position, FOV, monster state, items #### **Chat System** (Broadcast vs Player-Specific) - `chat_broadcast` - PUBLIC chat messages sent to ALL clients - `status_update` - PRIVATE status messages sent to individual players -- Commands (like "enter", "hello") trigger status updates, not chat broadcasts -- Real chat messages include username, timestamp, and message content +- Commands trigger status updates, real chat includes username and timestamp #### **Connection & Status Management** - `status_update` - Player-specific notifications (errors, confirmations, help) - `welcome` - Initial connection acknowledgment - `game_start` / `game_over` - Game lifecycle events -- Connection status immediately broadcast on client connect ### Socket Event Types @@ -150,65 +149,20 @@ io.to(socketId).emit('game_update', gameState); io.to(socketId).emit('game_start', gameState); io.to(socketId).emit('game_over', { status, reason, message }); -// Future Spectator Events (TODO) +// Future Spectator Events (Planned) io.to(spectatorId).emit('spectator_update', { playerSocketId, gameState }); ``` ### Future Spectator System (Roadmap) -The socket architecture is designed to support spectating with minimal changes: +The architecture supports adding spectator features with minimal changes: 1. **Spectator Registration**: Players can request to watch active games 2. **Multi-Cast Updates**: Game updates broadcast to player + all their spectators -3. **Spectator-Specific Events**: Special events for spectator UI (player info, game stats) +3. **Spectator-Specific Events**: Special events for spectator UI 4. **Spectator Chat**: Separate chat channels for spectators vs players 5. **Spectator Limits**: Configurable limits on spectators per game -### Real-Time Communication Goals - -- **Blockchain Sync**: All clients receive block updates every few seconds (not just on block changes) -- **Scalable Game Updates**: Player actions broadcast efficiently to relevant viewers only -- **Rich Chat System**: Distinction between commands/status and actual chat communication -- **Spectator Ready**: Architecture supports adding spectator features without major refactoring - -## Recent Major Improvements - -### ๐Ÿ”Œ Socket Architecture Overhaul -- **Implemented**: Structured event system for broadcast vs player-specific messages -- **Features**: Block height broadcasting, chat system separation, spectator-ready design -- **Helper Functions**: `sendGameUpdate()`, `broadcastBlockHeight()`, `sendStatusUpdate()` -- **Result**: Clean separation of concerns and foundation for spectator mode - -### ๐Ÿ’€ Death Logic Fix -- **Fixed**: Players not dying at correct block timing in autostart/debug mode -- **Solution**: Updated death logic to kill players the block after they enter -- **Result**: Players die exactly when block advances after entry (block N+1 after entering on block N) - -### โฐ Auto-Return to Title Screen -- **Added**: 30-second timeout after game over (win/lose/timeout) -- **Features**: Automatic return to welcome screen, proper game state reset -- **Result**: Seamless game flow without manual intervention - -### ๐ŸŽฏ Movement System Overhaul -- **Fixed**: Players skipping over tiles in hallways -- **Solution**: Added movement throttling (100ms) and prevented duplicate event handlers -- **Result**: Precise one-tile-per-keypress movement - -### ๐ŸŽฎ START Button Fix -- **Fixed**: START button invisible (cut off in game display area) -- **Solution**: Moved to HTML status area with proper styling and visibility control -- **Result**: Clearly visible green "๐ŸŽฎ START GAME" button - -### ๐Ÿš€ Auto-Start System -- **Added**: Immediate game start for development and testing -- **Features**: Debug mode detection, 'D' key shortcut, proper SocketHandlers initialization -- **Result**: Games start instantly in debug mode for faster development - -### ๐Ÿ—๏ธ Modular Architecture -- **Completed**: Broke down 786-line monolithic game.js into focused modules -- **Benefits**: Better maintainability, reusability, clean APIs, separation of concerns -- **Result**: Much cleaner and more maintainable codebase - ## Installation & Setup 1. **Clone the repository**: @@ -232,12 +186,12 @@ The socket architecture is designed to support spectating with minimal changes: 4. **Access the game**: - Open your browser to `http://localhost:3000` - In debug mode (localhost), press 'D' to start immediately - - In production mode, wait for block simulation and click "๐ŸŽฎ START GAME" + - In production mode, wait for block progression and enter the dungeon ## Game Controls - **WASD** or **Arrow Keys**: Move player -- **Enter** or **START Button**: Enter dungeon (when available) +- **Enter**: Enter dungeon (when available) - **D Key**: Debug instant start (localhost only) - **Chat**: Type commands and communicate with other players @@ -246,39 +200,43 @@ The socket architecture is designed to support spectating with minimal changes: When running locally (localhost/127.0.0.1), the game includes debug features: - **Immediate Start**: Press 'D' to bypass block timing -- **Debug Logs**: Enhanced console logging with timing information -- **Block Simulation**: 30-second intervals instead of real Wownero blocks +- **Enhanced Logging**: Detailed console logging with timing information +- **Block Simulation**: Fast intervals instead of real Wownero blocks - **Auto-Entry**: Instant game start for testing ## File Structure ``` wowngeon/ -โ”œโ”€โ”€ README.md # This file -โ”œโ”€โ”€ src/backend/ # Backend server code -โ”‚ โ”œโ”€โ”€ index.js # Main server file -โ”‚ โ”œโ”€โ”€ game.js # Game logic and mechanics -โ”‚ โ”œโ”€โ”€ user.js # User management -โ”‚ โ”œโ”€โ”€ player.js # Player state management -โ”‚ โ”œโ”€โ”€ monster.js # Monster AI -โ”‚ โ”œโ”€โ”€ dungeon.js # Dungeon generation -โ”‚ โ”œโ”€โ”€ dbcalls.js # Database operations -โ”‚ โ””โ”€โ”€ rpccalls.js # Blockchain RPC calls -โ”œโ”€โ”€ html/ # Frontend web interface -โ”‚ โ”œโ”€โ”€ index.html # Main game page -โ”‚ โ”œโ”€โ”€ js/ # Modular JavaScript -โ”‚ โ”‚ โ”œโ”€โ”€ displayManager.js # Display management -โ”‚ โ”‚ โ”œโ”€โ”€ screenManager.js # Screen states -โ”‚ โ”‚ โ”œโ”€โ”€ renderEngine.js # Game rendering -โ”‚ โ”‚ โ”œโ”€โ”€ gameState.js # Game state management -โ”‚ โ”‚ โ”œโ”€โ”€ game.js # Main game controller -โ”‚ โ”‚ โ”œโ”€โ”€ inputHandler.js # Input handling -โ”‚ โ”‚ โ”œโ”€โ”€ socketHandlers.js # WebSocket communication -โ”‚ โ”‚ โ”œโ”€โ”€ ui.js # User interface -โ”‚ โ”‚ โ””โ”€โ”€ options.js # Game configuration -โ”‚ โ”œโ”€โ”€ styles/ # CSS styling -โ”‚ โ””โ”€โ”€ tiles.png # Game tile graphics -โ””โ”€โ”€ test/ # Test files +โ”œโ”€โ”€ README.md # Project documentation +โ”œโ”€โ”€ src/backend/ # Backend server code +โ”‚ โ”œโ”€โ”€ index.js # Main server orchestrator (69 lines) +โ”‚ โ”œโ”€โ”€ socketHandlers.js # Socket event processing (400+ lines) +โ”‚ โ”œโ”€โ”€ broadcastManager.js # Communication management +โ”‚ โ”œโ”€โ”€ debugManager.js # Debug/production mode handling +โ”‚ โ”œโ”€โ”€ game.js # Game logic with factory methods +โ”‚ โ”œโ”€โ”€ user.js # User management class +โ”‚ โ”œโ”€โ”€ dungeon.js # Dungeon generation system +โ”‚ โ”œโ”€โ”€ lightingAndFov.js # Field of view calculations +โ”‚ โ”œโ”€โ”€ player.js # Player state management +โ”‚ โ”œโ”€โ”€ monster.js # Monster AI +โ”‚ โ”œโ”€โ”€ dbcalls.js # Database operations +โ”‚ โ””โ”€โ”€ rpccalls.js # Blockchain RPC calls +โ”œโ”€โ”€ html/ # Frontend web interface +โ”‚ โ”œโ”€โ”€ index.html # Main game page +โ”‚ โ”œโ”€โ”€ js/ # Modular JavaScript +โ”‚ โ”‚ โ”œโ”€โ”€ displayManager.js # Display management +โ”‚ โ”‚ โ”œโ”€โ”€ screenManager.js # Screen states +โ”‚ โ”‚ โ”œโ”€โ”€ renderEngine.js # Game rendering +โ”‚ โ”‚ โ”œโ”€โ”€ gameState.js # Game state management +โ”‚ โ”‚ โ”œโ”€โ”€ game.js # Main game controller +โ”‚ โ”‚ โ”œโ”€โ”€ inputHandler.js # Input handling +โ”‚ โ”‚ โ”œโ”€โ”€ socketHandlers.js # WebSocket communication +โ”‚ โ”‚ โ”œโ”€โ”€ ui.js # User interface +โ”‚ โ”‚ โ””โ”€โ”€ options.js # Game configuration +โ”‚ โ”œโ”€โ”€ styles/ # CSS styling +โ”‚ โ””โ”€โ”€ tiles.png # Game tile graphics +โ””โ”€โ”€ test/ # Test files ``` ## Contributing diff --git a/src/backend/broadcastManager.js b/src/backend/broadcastManager.js new file mode 100644 index 0000000..7cc012e --- /dev/null +++ b/src/backend/broadcastManager.js @@ -0,0 +1,189 @@ +/** + * Broadcast Manager Module + * Handles all socket.io event broadcasting for the Wowngeon game server + * Supports current player-specific events and future spectator broadcasting + */ + +class BroadcastManager { + constructor(io) { + this.io = io; + } + + // ====== GLOBAL BROADCASTS (All Clients) ====== + + /** + * Broadcast block height to all connected clients + * @param {number} blockHeight - Current block height + */ + broadcastBlockHeight(blockHeight) { + console.log(`๐Ÿ“ก Broadcasting block height ${blockHeight} to all clients`); + this.io.emit('blockheight', blockHeight); + } + + /** + * Broadcast chat message to all connected clients + * @param {string} username - Username of the sender + * @param {string} message - Chat message content + * @param {number} timestamp - Message timestamp + * @param {string} socketId - Socket ID of the sender + */ + broadcastChatMessage(username, message, timestamp, socketId) { + console.log(`๐Ÿ’ฌ Broadcasting chat message from ${socketId}: "${message}"`); + + this.io.emit('chat_broadcast', { + username: username, + message: message, + timestamp: timestamp, + socketId: socketId + }); + } + + // ====== PLAYER-SPECIFIC BROADCASTS ====== + + /** + * Send status update to a specific player + * @param {string} socketId - The socket ID of the player + * @param {string} type - Type of status (info, error, warning, connection, help, etc.) + * @param {string} message - The status message + */ + sendStatusUpdate(socketId, type, message) { + this.io.to(socketId).emit('status_update', { + type: type, + message: message, + timestamp: Date.now() + }); + } + + /** + * Send welcome message to a newly connected client + * @param {string} clientId - The client ID + */ + sendWelcome(clientId) { + this.io.to(clientId).emit('welcome', clientId); + } + + /** + * Send game state update to player and any spectators + * @param {string} playerSocketId - The socket ID of the player + * @param {object} gameState - The game state to broadcast + */ + sendGameUpdate(playerSocketId, gameState) { + // Debug logging for lighting data in updates + console.log(`๐Ÿ“ก sendGameUpdate to ${playerSocketId}:`); + console.log(` - Lighting data: ${!!gameState.lighting} (${gameState.lighting ? Object.keys(gameState.lighting).length : 0} rows)`); + console.log(` - Torch data: ${!!gameState.torches} (${gameState.torches ? gameState.torches.length : 0} torches)`); + + // Send to the player + this.io.to(playerSocketId).emit('game_update', gameState); + + // TODO: Future spectator support + // Get list of spectators for this game and broadcast to them too + // const spectators = getSpectatorsForPlayer(playerSocketId); + // spectators.forEach(spectatorId => { + // this.io.to(spectatorId).emit('spectator_update', { + // playerSocketId: playerSocketId, + // gameState: gameState + // }); + // }); + } + + /** + * Send game start event to player + * @param {string} socketId - The socket ID of the player + * @param {object} gameState - Initial game state + */ + sendGameStart(socketId, gameState) { + console.log(`๐ŸŽฎ SENDING GAME_START to ${socketId}`); + this.io.to(socketId).emit('game_start', gameState); + } + + /** + * Send game over event to player + * @param {string} socketId - The socket ID of the player + * @param {object} gameOverData - Game over information + */ + sendGameOver(socketId, gameOverData) { + this.io.to(socketId).emit('game_over', gameOverData); + } + + /** + * Send waiting status to player (for queue system) + * @param {string} socketId - The socket ID of the player + * @param {object} waitingData - Waiting status information + */ + sendWaitingStatus(socketId, waitingData) { + this.io.to(socketId).emit('waiting_status', waitingData); + } + + /** + * Send queue cancelled event to player + * @param {string} socketId - The socket ID of the player + */ + sendQueueCancelled(socketId) { + this.io.to(socketId).emit('queue_cancelled'); + } + + /** + * Send simple message to player + * @param {string} socketId - The socket ID of the player + * @param {string} message - The message content + */ + sendMessage(socketId, message) { + this.io.to(socketId).emit('message', message); + } + + /** + * Send socket registration confirmation + * @param {string} socketId - The socket ID + * @param {object} registrationData - Registration confirmation data + */ + sendSocketRegistered(socketId, registrationData) { + this.io.to(socketId).emit('socket_registered', registrationData); + } + + /** + * Send debug pong response + * @param {string} socketId - The socket ID + * @param {object} pongData - Debug pong data + */ + sendDebugPong(socketId, pongData) { + this.io.to(socketId).emit('debug_pong', pongData); + } + + // ====== FUTURE SPECTATOR SUPPORT ====== + + /** + * Get spectators for a specific player's game + * @param {string} playerSocketId - The player's socket ID + * @returns {string[]} Array of spectator socket IDs + * TODO: Implement spectator tracking system + */ + getSpectatorsForPlayer(playerSocketId) { + // Future implementation: return array of spectator socket IDs + return []; + } + + /** + * Add spectator to a player's game + * @param {string} playerSocketId - The player's socket ID + * @param {string} spectatorSocketId - The spectator's socket ID + * TODO: Implement spectator tracking system + */ + addSpectator(playerSocketId, spectatorSocketId) { + // Future implementation: track spectators per game + console.log(`TODO: Add spectator ${spectatorSocketId} to watch player ${playerSocketId}`); + } + + /** + * Remove spectator from a player's game + * @param {string} playerSocketId - The player's socket ID + * @param {string} spectatorSocketId - The spectator's socket ID + * TODO: Implement spectator tracking system + */ + removeSpectator(playerSocketId, spectatorSocketId) { + // Future implementation: remove spectator tracking + console.log(`TODO: Remove spectator ${spectatorSocketId} from watching player ${playerSocketId}`); + } +} + +module.exports = BroadcastManager; diff --git a/src/backend/debugManager.js b/src/backend/debugManager.js new file mode 100644 index 0000000..b7c1fd5 --- /dev/null +++ b/src/backend/debugManager.js @@ -0,0 +1,227 @@ +/** + * Debug Manager Module + * Handles debug functionality and simulated block height logic for development + * Keeps debug/development code separate from production logic + */ + +const rpc = require('./rpccalls.js'); + +class DebugManager { + constructor(broadcastManager) { + this.broadcastManager = broadcastManager; + this.DEBUG_MODE = true; // Set to false to disable debug mode + this.debugBlockHeight = 1; + this.debugInterval = null; + this.statusInterval = null; + this.lastProductionBlockHeight = 0; + } + + // ====== INITIALIZATION ====== + + /** + * Initialize debug mode or production mode + */ + initialize() { + if (this.DEBUG_MODE) { + this.initializeDebugMode(); + } else { + this.initializeProductionMode(); + } + } + + /** + * Initialize debug mode with simulated blocks + */ + initializeDebugMode() { + console.log("๐Ÿ› DEBUG MODE ENABLED - Simulating blocks every 30 seconds"); + + // Initial debug block broadcast + this.broadcastManager.broadcastBlockHeight(this.debugBlockHeight); + + // Debug block height simulator - advances every 30 seconds + this.debugInterval = setInterval(() => { + this.debugBlockHeight++; + console.log(`๐Ÿ› DEBUG: New simulated block: ${this.debugBlockHeight}`); + this.broadcastManager.broadcastBlockHeight(this.debugBlockHeight); + + // Notify listeners about new block + this.onNewBlock(this.debugBlockHeight); + + }, 30000); // Every 30 seconds + + // Regular status broadcasting - sends current block height every 5 seconds + // This ensures all clients stay up-to-date even if they miss a block change + this.statusInterval = setInterval(() => { + this.broadcastManager.broadcastBlockHeight(this.debugBlockHeight); + }, 5000); // Every 5 seconds + } + + /** + * Initialize production mode with real blockchain calls + */ + initializeProductionMode() { + console.log("๐Ÿš€ PRODUCTION MODE ENABLED - Using real blockchain RPC calls"); + + // Real blockchain monitoring + this.debugInterval = setInterval(() => { + rpc.daemonCall("get_block_count", "", (result) => { + if (!result || !result.result || !result.result.count) { + console.log("โŒ Failed to get block count from daemon"); + return; + } + + const currentHeight = result.result.count; + rpc.lastBlock.setHeight(currentHeight); + + // If new block found + if (currentHeight > this.lastProductionBlockHeight) { + console.log(`โ›๏ธ New block found: ${currentHeight}`); + this.broadcastManager.broadcastBlockHeight(currentHeight); + + // Notify listeners about new block + this.onNewBlock(currentHeight); + + this.lastProductionBlockHeight = currentHeight; + } else { + // Still broadcast current height for status updates + this.broadcastManager.broadcastBlockHeight(currentHeight); + } + }); + }, 5000); // Every 5 seconds + } + + // ====== BLOCK HEIGHT MANAGEMENT ====== + + /** + * Get the current block height (debug or production) + * @returns {number} Current block height + */ + getCurrentBlockHeight() { + return this.DEBUG_MODE ? this.debugBlockHeight : this.lastProductionBlockHeight; + } + + /** + * Set debug block height (debug mode only) + * @param {number} height - New block height + */ + setDebugBlockHeight(height) { + if (this.DEBUG_MODE) { + this.debugBlockHeight = height; + this.broadcastManager.broadcastBlockHeight(this.debugBlockHeight); + } else { + console.warn("Cannot set debug block height in production mode"); + } + } + + /** + * Advance debug block height by one (debug mode only) + */ + advanceDebugBlock() { + if (this.DEBUG_MODE) { + this.debugBlockHeight++; + console.log(`๐Ÿ› DEBUG: Manually advanced block to: ${this.debugBlockHeight}`); + this.broadcastManager.broadcastBlockHeight(this.debugBlockHeight); + this.onNewBlock(this.debugBlockHeight); + } else { + console.warn("Cannot advance debug block in production mode"); + } + } + + // ====== EVENT HANDLING ====== + + /** + * Called when a new block is detected (debug or production) + * Override this method or set callbacks to handle new blocks + * @param {number} blockHeight - The new block height + */ + onNewBlock(blockHeight) { + // This method can be overridden or have callbacks registered + console.log(`๐Ÿ“ฆ New block detected: ${blockHeight}`); + + // Call registered callbacks + if (this.newBlockCallbacks) { + this.newBlockCallbacks.forEach(callback => { + try { + callback(blockHeight); + } catch (error) { + console.error("Error in new block callback:", error); + } + }); + } + } + + /** + * Register a callback for when new blocks are detected + * @param {function} callback - Function to call with new block height + */ + onNewBlockCallback(callback) { + if (!this.newBlockCallbacks) { + this.newBlockCallbacks = []; + } + this.newBlockCallbacks.push(callback); + } + + // ====== DEBUG UTILITIES ====== + + /** + * Get debug status information + * @returns {object} Debug status object + */ + getDebugStatus() { + return { + debugMode: this.DEBUG_MODE, + currentBlockHeight: this.getCurrentBlockHeight(), + isRunning: !!this.debugInterval, + lastProductionBlock: this.lastProductionBlockHeight, + debugBlock: this.debugBlockHeight + }; + } + + /** + * Log debug socket event + * @param {string} socketId - Socket ID + * @param {string} eventName - Event name + * @param {object} data - Event data + */ + debugSocket(socketId, eventName, data) { + const shortSocketId = socketId.substring(0, 8); + console.log(`๐Ÿ”Œ SOCKET DEBUG: Sending ${eventName} to ${shortSocketId}...`); + + // Limit payload logging to prevent console spam + const dataStr = JSON.stringify(data); + const shortDataStr = dataStr.length > 300 ? dataStr.substring(0, 300) + "..." : dataStr; + console.log(`๐Ÿ“ฆ PAYLOAD: ${shortDataStr}`); + } + + // ====== CLEANUP ====== + + /** + * Stop all debug intervals and clean up + */ + cleanup() { + if (this.debugInterval) { + clearInterval(this.debugInterval); + this.debugInterval = null; + } + + if (this.statusInterval) { + clearInterval(this.statusInterval); + this.statusInterval = null; + } + + console.log("๐Ÿงน Debug manager cleaned up"); + } + + /** + * Toggle debug mode (restart required) + * @param {boolean} enabled - Whether to enable debug mode + */ + setDebugMode(enabled) { + console.log(`๐Ÿ”„ Switching debug mode to: ${enabled ? 'ENABLED' : 'DISABLED'}`); + this.cleanup(); + this.DEBUG_MODE = enabled; + this.initialize(); + } +} + +module.exports = DebugManager; diff --git a/src/backend/index.js b/src/backend/index.js index 58a22e8..59cc5eb 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -3,595 +3,66 @@ const path = require('path'); var app = 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 +// Import modular components +const BroadcastManager = require('./broadcastManager'); +const DebugManager = require('./debugManager'); +const SocketHandlers = require('./socketHandlers'); -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'); +// Initialize modular components +const broadcastManager = new BroadcastManager(io); +const debugManager = new DebugManager(broadcastManager); const activeGames = new Map(); // Maps socketId to Game objects -const playerMoveTimestamps = new Map(); // Track last move time per player -const MOVE_COOLDOWN = 100; // Minimum 100ms between moves on server side - -/** - * Create a new game for a user and register it in activeGames - * @param {User} user - The user object - * @param {string} gameType - 'standard' or 'legacy' - * @param {object} options - Additional game options - */ -function createGameForUser(user, gameType = 'standard', options = {}) { - let game; - - if (gameType === 'legacy') { - game = Game.createLegacyGame(user.id, user, options); - } else { - game = Game.createStandardGame(user.id, user, options); - } - - // Register the game with the user and in activeGames - user.joinGame(game); - activeGames.set(user.id, game); - - console.log(`[createGameForUser] Created ${gameType} game ${game.id} for user ${user.id}`); - return game; -} -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 - -// Use absolute path to html directory to ensure static files are served correctly +const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager); +// Configure static file serving const htmlPath = path.join(__dirname, '../../html'); app.use(express.static(htmlPath)); +app.use(express.json()); // Parse JSON for API endpoints + +// Serve main page app.get('/', function(req, res) { res.sendFile('index.html', { root: htmlPath }); }); -// 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 broadcast - broadcastBlockHeight(debugBlockHeight); - - // Debug block height simulator - advances every 30 seconds - setInterval(function() { - debugBlockHeight++; - console.log(`๐Ÿ› DEBUG: New simulated block: ${debugBlockHeight}`); - broadcastBlockHeight(debugBlockHeight); - - // Start games for waiting players - startGamesForWaiting(debugBlockHeight); - - // Check active games for timeout - checkGamesTimeout(debugBlockHeight); - - }, 30000); // Every 30 seconds - - // Regular status broadcasting - sends current block height every 5 seconds - // This ensures all clients stay up-to-date even if they miss a block change - setInterval(function() { - broadcastBlockHeight(debugBlockHeight); - }, 5000); // Every 5 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 (QUEUE ENTRY METHOD) -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) { - // For queue entry, player gets 3 blocks starting from their entry block - currentUser.blockRec = blockHeight; - console.log(`๐Ÿ•’ QUEUE ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); - - try { - const game = createGameForUser(currentUser, 'standard'); - - 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); - - // Players die the block after they enter - // If they entered on block 1, they die when block 2 starts - // If they entered on block 2, they die when block 3 starts - if (user && user.blockRec && currentHeight > user.blockRec) { - console.log(`๐Ÿ’€ GAME TIMEOUT for player ${socketId}: entered on block ${user.blockRec}, died on block ${currentHeight}`); - - // 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 EVENT BROADCASTING HELPERS ====== -// These functions abstract the broadcasting logic to support future features like spectating - -/** - * Send game state update to player and any spectators - * @param {string} playerSocketId - The socket ID of the player - * @param {object} gameState - The game state to broadcast - */ -function sendGameUpdate(playerSocketId, gameState) { - // Debug logging for lighting data in updates - console.log(`๐Ÿ“ก sendGameUpdate to ${playerSocketId}:`); - console.log(` - Lighting data: ${!!gameState.lighting} (${gameState.lighting ? Object.keys(gameState.lighting).length : 0} rows)`); - console.log(` - Torch data: ${!!gameState.torches} (${gameState.torches ? gameState.torches.length : 0} torches)`); - - // Send to the player - io.to(playerSocketId).emit('game_update', gameState); - - // TODO: Future spectator support - // Get list of spectators for this game and broadcast to them too - // const spectators = getSpectatorsForPlayer(playerSocketId); - // spectators.forEach(spectatorId => { - // io.to(spectatorId).emit('spectator_update', { - // playerSocketId: playerSocketId, - // gameState: gameState - // }); - // }); -} - -/** - * Broadcast block height to all connected clients - * @param {number} blockHeight - Current block height - */ -function broadcastBlockHeight(blockHeight) { - console.log(`๐Ÿ“ก Broadcasting block height ${blockHeight} to all clients`); - io.emit('blockheight', blockHeight); -} - -/** - * Send status update to a specific player - * @param {string} socketId - The socket ID of the player - * @param {string} type - Type of status (info, error, warning, etc.) - * @param {string} message - The status message - */ -function sendStatusUpdate(socketId, type, message) { - io.to(socketId).emit('status_update', { - type: type, - message: message, - timestamp: Date.now() - }); -} - -// 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 and immediate status - io.to(socket.client.id).emit('welcome', socket.client.id); - - // Send current block height immediately to new connections - const currentBlock = DEBUG_MODE ? debugBlockHeight : lastBlockHeight; - console.log(`๐Ÿ“ˆ Sending current block height ${currentBlock} to new connection ${socket.id}`); - io.to(socket.id).emit('blockheight', currentBlock); - - // Send connection status update (player-specific) - sendStatusUpdate(socket.id, 'connection', 'Connected to Wowngeon server'); - - socket.on('chat message', function(msg) { - console.log('Message received:', msg); - - // Handle game commands (player-specific responses) - if (msg.toLowerCase() == 'hello') { - // Send status update to this player only - sendStatusUpdate(socket.id, 'help', - '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.'); - return; // Don't broadcast commands as chat - } - - // Handle game entry commands - if (msg.toLowerCase() == 'enter') { - console.log(`Player ${socket.id} requested to enter the dungeon - STARTING IMMEDIATELY`); - - // Use the updated user lookup function - const currentUser = getUserBySocket(socket.id); - - if (currentUser) { - console.log(`Found user for socket ${socket.id}, starting game immediately...`); - - // Start game immediately for faster testing - try { - // For auto-entry in debug mode, player enters on current block - // They will die when the next block starts - const currentBlock = debugBlockHeight || lastBlockHeight || 1; - currentUser.blockRec = currentBlock; // Player enters on current block - console.log(`๐Ÿ•’ AUTO-ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); - - const game = createGameForUser(currentUser, 'standard'); - - const gameState = game.getState(); - gameState.blockHeight = currentBlock; - - console.log(`๐ŸŽฎ SENDING IMMEDIATE GAME_START to ${socket.id}`); - io.to(socket.id).emit('game_start', gameState); - console.log(`Game started immediately for player ${socket.id}`); - } catch (error) { - console.error(`Error creating game:`, error); - sendStatusUpdate(socket.id, 'error', 'Error starting game: ' + error.message); - } - } else { - sendStatusUpdate(socket.id, 'error', 'Error: Could not start game. Please try again.'); - } - return; // Don't broadcast commands as chat - } - // 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); - sendStatusUpdate(socket.id, 'info', 'You have left the queue.'); - - // Reset to welcome screen - io.to(socket.id).emit('queue_cancelled'); - } else { - // Send status update to this player only - sendStatusUpdate(socket.id, 'error', 'You were not in the queue.'); - } - return; // Don't broadcast commands as chat - } - - // For all other messages (actual chat), broadcast to all clients - else { - console.log(`๐Ÿ’ฌ Broadcasting chat message from ${socket.id}: "${msg}"`); - - // Get user info for chat display - const currentUser = getUserBySocket(socket.id); - const username = currentUser?.username || `User_${socket.id.substr(-4)}`; - - // Broadcast chat message to ALL clients - io.emit('chat_broadcast', { - username: username, - message: msg, - timestamp: Date.now(), - socketId: socket.id - }); - } - }); - - // Handle player movement - socket.on('player_move', function(moveData) { - console.log(`Player move event received from ${socket.id}:`, moveData); - const currentUser = getUserBySocket(socket.id); - const game = activeGames.get(socket.id); - - if (currentUser && game && game.gameState === 'active') { - // Server-side movement throttling - const now = Date.now(); - const lastMoveTime = playerMoveTimestamps.get(socket.id) || 0; - - if (now - lastMoveTime < MOVE_COOLDOWN) { - console.log(`Move from ${socket.id} throttled - too soon after last move (${now - lastMoveTime}ms)`); - return; // Ignore move if too soon - } - - if (typeof moveData.dx === 'number' && typeof moveData.dy === 'number') { - playerMoveTimestamps.set(socket.id, now); // Update timestamp before processing - - 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'; - game.endGame('lost', { score: 0, reason: 'monster' }); - 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'; - const score = game.player.hasTreasure ? 100 : 50; // Bonus for treasure - game.endGame('won', { - score: score, - reason: 'escaped', - treasuresFound: game.player.hasTreasure ? 1 : 0 - }); - io.to(socket.id).emit('game_over', { - status: 'won', - reason: 'escaped', - message: 'Congratulations! You escaped the dungeon!', - score: score - }); - activeGames.delete(socket.id); - return; - } - - const updatedGameState = game.getState(); // Get the new state - - // Debug logging for lighting data in game updates - console.log(`๐Ÿ” GAME UPDATE DEBUG for ${socket.id}:`); - console.log(` - Player position: (${updatedGameState.player?.x}, ${updatedGameState.player?.y})`); - console.log(` - Visible tiles keys: ${Object.keys(updatedGameState.visibleTiles || {}).length} rows`); - console.log(` - Lighting data included: ${!!updatedGameState.lighting}`); - if (updatedGameState.lighting) { - const lightingTileCount = Object.keys(updatedGameState.lighting).reduce((acc, yKey) => - acc + Object.keys(updatedGameState.lighting[yKey] || {}).length, 0); - console.log(` - Lighting tiles count: ${lightingTileCount}`); - } - console.log(` - Torch data included: ${!!updatedGameState.torches}`); - if (updatedGameState.torches) { - console.log(` - Torch count: ${updatedGameState.torches.length}`); - } - - // Log before sending to client for debugging - console.log(`Sending game_update to ${socket.id} after player move.`); - - // Send game update to player (current implementation) - // TODO: In future, also broadcast to spectators of this game - sendGameUpdate(socket.id, 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(); - // sendGameUpdate(socket.id, 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); - - // Clean up movement timestamps - playerMoveTimestamps.delete(socket.id); - - // Clean up active games - activeGames.delete(socket.id); - - // Remove from waiting players - const waitingIndex = WAITING_PLAYERS.findIndex(p => p.serverId === socket.id); - if (waitingIndex !== -1) { - WAITING_PLAYERS.splice(waitingIndex, 1); - } - - 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 = createGameForUser(currentUser, 'legacy'); - 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)}...`); -} - -// Add express middleware to parse JSON -app.use(express.json()); -app.use(express.static('html')); - // Debug endpoint to receive client-side debug info app.post('/debug', (req, res) => { - const debugData = req.body; - console.log('๐ŸŽฏ CLIENT DEBUG:', debugData.message || debugData); - res.json({ status: 'ok' }); + console.log('๐Ÿ“ CLIENT DEBUG INFO:', req.body); + res.json({ received: true, timestamp: Date.now() }); +}); + +// Initialize debug manager with new block callbacks +debugManager.onNewBlockCallback((blockHeight) => { + // Start games for waiting players when new block detected + socketHandlers.startGamesForWaiting(blockHeight); + // Check active games for timeout + socketHandlers.checkGamesTimeout(blockHeight); +}); + +// Start the debug/production system +debugManager.initialize(); + +// Socket.io connection handler +io.on('connection', function(socket) { + socketHandlers.handleConnection(socket); +}); + +// Debug function for registered users +function debugRegisteredUsers() { + console.log("๐Ÿ“Š REGISTERED USERS DEBUG:"); + const allUsers = require('./user').getAllUsers(); + console.log(`Total registered users: ${allUsers.length}`); + allUsers.forEach(user => { + console.log(` - User ID: ${user.id}, Socket: ${user.socketId}, Client: ${user.clientId}`); + }); + console.log(`Active games: ${activeGames.size}`); +} + +// Regular debug logging +setInterval(debugRegisteredUsers, 10000); + +// Start server +http.listen(3000, function() { + console.log('๐Ÿš€ Wowngeon server listening on *:3000'); + console.log(`๐Ÿ› Debug mode: ${debugManager.getDebugStatus().debugMode ? 'ENABLED' : 'DISABLED'}`); }); diff --git a/src/backend/socketHandlers.js b/src/backend/socketHandlers.js new file mode 100644 index 0000000..7618a3c --- /dev/null +++ b/src/backend/socketHandlers.js @@ -0,0 +1,415 @@ +/** + * Socket Event Handlers Module + * Handles all socket.io event processing for the Wowngeon game server + */ + +const user = require('./user'); +const Game = require('./game'); + +class SocketHandlers { + constructor(io, activeGames, broadcastManager, debugManager) { + this.io = io; + this.activeGames = activeGames; + this.broadcastManager = broadcastManager; + this.debugManager = debugManager; + this.playerMoveTimestamps = new Map(); + this.clientSocketMap = new Map(); + this.MOVE_COOLDOWN = 100; // Minimum 100ms between moves + this.WAITING_PLAYERS = []; // Players waiting for the next block + } + + /** + * Initialize socket event handlers for a new connection + */ + handleConnection(socket) { + console.log('A user connected'); + console.log(socket.client.id); + console.log(socket.handshake.address); + + // Create and register user + new user.User(socket.id, socket.handshake.address); + const newUser = this.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})`); + } + + // Send welcome and current status + this.io.to(socket.client.id).emit('welcome', socket.client.id); + + // Send current block height + const currentBlock = this.debugManager.getCurrentBlockHeight(); + console.log(`๐Ÿ“ˆ Sending current block height ${currentBlock} to new connection ${socket.id}`); + this.io.to(socket.id).emit('blockheight', currentBlock); + + // Send connection status + this.broadcastManager.sendStatusUpdate(socket.id, 'connection', 'Connected to Wowngeon server'); + + // Register event handlers + socket.on('chat message', (msg) => this.handleChatMessage(socket, msg)); + socket.on('player_move', (moveData) => this.handlePlayerMove(socket, moveData)); + socket.on('disconnect', () => this.handleDisconnect(socket)); + socket.on('debug_ping', (data) => this.handleDebugPing(socket, data)); + socket.on('register_client', (data) => this.handleRegisterClient(socket, data)); + } + + /** + * Handle chat messages and game commands + */ + handleChatMessage(socket, msg) { + console.log('Message received:', msg); + + const command = msg.toLowerCase(); + + // Handle game commands + switch (command) { + case 'hello': + this.broadcastManager.sendStatusUpdate(socket.id, 'help', + '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.'); + return; + + case 'enter': + this.handleGameEntry(socket); + return; + + case 'cancel': + this.handleCancelEntry(socket); + return; + + default: + // Broadcast as chat message to all clients + this.handleChatBroadcast(socket, msg); + } + } + + /** + * Handle game entry request + */ + handleGameEntry(socket) { + console.log(`Player ${socket.id} requested to enter the dungeon - STARTING IMMEDIATELY`); + + const currentUser = this.getUserBySocket(socket.id); + if (!currentUser) { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Error: Could not start game. Please try again.'); + return; + } + + console.log(`Found user for socket ${socket.id}, starting game immediately...`); + + try { + // For auto-entry in debug mode, player enters on current block + const currentBlock = this.debugManager.getCurrentBlockHeight(); + currentUser.blockRec = currentBlock; + console.log(`๐Ÿ•’ AUTO-ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); + + const game = this.createGameForUser(currentUser, 'standard'); + + const gameState = game.getState(); + gameState.blockHeight = currentBlock; + + console.log(`๐ŸŽฎ SENDING IMMEDIATE GAME_START to ${socket.id}`); + this.io.to(socket.id).emit('game_start', gameState); + console.log(`Game started immediately for player ${socket.id}`); + } catch (error) { + console.error(`Error creating game:`, error); + this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Error starting game: ' + error.message); + } + } + + /** + * Handle cancel entry request + */ + handleCancelEntry(socket) { + const index = this.WAITING_PLAYERS.findIndex(p => p.serverId === socket.id); + + if (index !== -1) { + this.WAITING_PLAYERS.splice(index, 1); + this.broadcastManager.sendStatusUpdate(socket.id, 'info', 'You have left the queue.'); + this.io.to(socket.id).emit('queue_cancelled'); + } else { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'You were not in the queue.'); + } + } + + /** + * Handle chat broadcast to all clients + */ + handleChatBroadcast(socket, msg) { + console.log(`๐Ÿ’ฌ Broadcasting chat message from ${socket.id}: "${msg}"`); + + const currentUser = this.getUserBySocket(socket.id); + const username = currentUser?.username || `User_${socket.id.substr(-4)}`; + + this.io.emit('chat_broadcast', { + username: username, + message: msg, + timestamp: Date.now(), + socketId: socket.id + }); + } + + /** + * Handle player movement + */ + handlePlayerMove(socket, moveData) { + console.log(`Player move event received from ${socket.id}:`, moveData); + const currentUser = this.getUserBySocket(socket.id); + const game = this.activeGames.get(socket.id); + + if (!currentUser || !game || game.gameState !== 'active') { + console.log(`Player move event from ${socket.id} ignored: No active game found for user.`); + return; + } + + // Server-side movement throttling + const now = Date.now(); + const lastMoveTime = this.playerMoveTimestamps.get(socket.id) || 0; + + if (now - lastMoveTime < this.MOVE_COOLDOWN) { + console.log(`Move from ${socket.id} throttled - too soon after last move (${now - lastMoveTime}ms)`); + return; + } + + if (typeof moveData.dx !== 'number' || typeof moveData.dy !== 'number') { + console.error(`Invalid moveData received from ${socket.id}:`, moveData); + return; + } + + this.playerMoveTimestamps.set(socket.id, now); + const moveResult = game.movePlayer(moveData.dx, moveData.dy); + + if (!moveResult || moveResult.status !== 'moved') { + console.log(`Player move from ${socket.id} was invalid or resulted in no change.`); + return; + } + + // Move monster after player moves + game.moveMonster(); + + // Check if monster caught player + if (this.checkMonsterKill(game.player, game.monster)) { + this.handleGameOver(socket, game, 'lost', 'monster', 'The monster caught you!'); + return; + } + + // Check for treasure pickup + if (moveResult.event === 'treasure_found') { + this.io.to(socket.id).emit('message', 'You found the treasure!'); + } + + // Check for escape + if (moveResult.event === 'escaped') { + const score = game.player.hasTreasure ? 100 : 50; + this.handleGameOver(socket, game, 'won', 'escaped', 'Congratulations! You escaped the dungeon!', score); + return; + } + + // Send game update + const updatedGameState = game.getState(); + this.logGameUpdate(socket.id, updatedGameState); + this.broadcastManager.sendGameUpdate(socket.id, updatedGameState); + } + + /** + * Handle game over scenarios + */ + handleGameOver(socket, game, status, reason, message, score = 0) { + game.gameState = status; + const gameStats = { + score: score, + reason: reason, + treasuresFound: game.player.hasTreasure ? 1 : 0 + }; + game.endGame(status, gameStats); + + this.io.to(socket.id).emit('game_over', { + status: status, + reason: reason, + message: message, + score: score + }); + + this.activeGames.delete(socket.id); + } + + /** + * Handle client disconnection + */ + handleDisconnect(socket) { + console.log('User disconnected', socket.client.id); + + // Clean up movement timestamps + this.playerMoveTimestamps.delete(socket.id); + + // Clean up active games + this.activeGames.delete(socket.id); + + // Remove from waiting players + const waitingIndex = this.WAITING_PLAYERS.findIndex(p => p.serverId === socket.id); + if (waitingIndex !== -1) { + this.WAITING_PLAYERS.splice(waitingIndex, 1); + } + + user.removeUser(socket.client.id); + } + + /** + * Handle debug ping + */ + handleDebugPing(socket, 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 + }); + } + + /** + * Handle client registration + */ + handleRegisterClient(socket, data) { + console.log(`Client registered: ${socket.id} (server) <-> ${data.clientId} (client)`); + + this.clientSocketMap.set(data.clientId, socket.id); + this.clientSocketMap.set(socket.id, data.clientId); + + socket.emit('socket_registered', { + clientId: data.clientId, + serverId: socket.id, + success: true + }); + } + + // Helper methods + + /** + * Get user by socket ID with fallback mapping + */ + getUserBySocket(socketId) { + console.log(`Looking up user with socketId: ${socketId}`); + + let foundUser = user.getUserBySocketId(socketId); + + if (!foundUser && this.clientSocketMap.has(socketId)) { + const mappedId = this.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; + } + + /** + * Create a new game for a user + */ + createGameForUser(user, gameType = 'standard', options = {}) { + let game; + + if (gameType === 'legacy') { + game = Game.createLegacyGame(user.id, user, options); + } else { + game = Game.createStandardGame(user.id, user, options); + } + + user.joinGame(game); + this.activeGames.set(user.id, game); + + console.log(`[createGameForUser] Created ${gameType} game ${game.id} for user ${user.id}`); + return game; + } + + /** + * Check if monster caught player + */ + checkMonsterKill(player, monster) { + return monster.x === player.x && monster.y === player.y; + } + + /** + * Log game update debug information + */ + logGameUpdate(socketId, gameState) { + console.log(`๐Ÿ” GAME UPDATE DEBUG for ${socketId}:`); + console.log(` - Player position: (${gameState.player?.x}, ${gameState.player?.y})`); + console.log(` - Visible tiles keys: ${Object.keys(gameState.visibleTiles || {}).length} rows`); + console.log(` - Lighting data included: ${!!gameState.lighting}`); + if (gameState.lighting) { + const lightingTileCount = Object.keys(gameState.lighting).reduce((acc, yKey) => + acc + Object.keys(gameState.lighting[yKey] || {}).length, 0); + console.log(` - Lighting tiles count: ${lightingTileCount}`); + } + console.log(` - Torch data included: ${!!gameState.torches}`); + if (gameState.torches) { + console.log(` - Torch count: ${gameState.torches.length}`); + } + console.log(`Sending game_update to ${socketId} after player move.`); + } + + // Queue management methods (for future block-based game entry) + + /** + * Start games for waiting players when a new block is found + */ + startGamesForWaiting(blockHeight) { + console.log(`Starting games for ${this.WAITING_PLAYERS.length} waiting players at block height ${blockHeight}`); + + while (this.WAITING_PLAYERS.length > 0) { + const playerEntry = this.WAITING_PLAYERS.shift(); + const serverId = playerEntry.serverId; + + console.log(`Processing player: server=${serverId}`); + + const currentUser = this.getUserBySocket(serverId); + + if (currentUser) { + currentUser.blockRec = blockHeight; + console.log(`๐Ÿ•’ QUEUE ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); + + try { + const game = this.createGameForUser(currentUser, 'standard'); + + const gameState = game.getState(); + gameState.blockHeight = blockHeight; + + console.log(`๐ŸŽฎ SENDING GAME_START to ${serverId}`); + this.io.to(serverId).emit('game_start', gameState); + console.log(`Game started for player ${serverId}`); + } catch (error) { + console.error(`Error creating game:`, error); + this.io.to(serverId).emit('message', 'Error starting game: ' + error.message); + } + } else { + console.error(`User not found for socket ${serverId}`); + this.io.to(serverId).emit('message', 'Error: User not found'); + } + } + } + + /** + * Check for game timeouts based on block height + */ + checkGamesTimeout(currentHeight) { + this.activeGames.forEach((game, socketId) => { + const user = this.getUserBySocket(socketId); + + if (user && user.blockRec && currentHeight > user.blockRec) { + console.log(`๐Ÿ’€ GAME TIMEOUT for player ${socketId}: entered on block ${user.blockRec}, died on block ${currentHeight}`); + + game.gameState = 'lost'; + this.io.to(socketId).emit('game_over', { + status: 'lost', + reason: 'timeout', + message: 'You didn\'t escape before the block time limit!' + }); + + this.activeGames.delete(socketId); + } + }); + } +} + +module.exports = SocketHandlers;