mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-22 00:28:34 +02:00
backend refactoring
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+50
-579
@@ -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'}`);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user