Files
wownerogue/src/network/socketHandlers.js
T

416 lines
15 KiB
JavaScript

/**
* Socket Event Handlers Module
* Handles all socket.io event processing for the Wowngeon game server
*/
const user = require('../db/user');
const Game = require('../game/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;