602 lines
45 KiB
JavaScript
602 lines
45 KiB
JavaScript
// /server/game/GameManager.js
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const GameInstance = require('./instance/GameInstance');
|
||
const dataUtils = require('../data/dataUtils');
|
||
const GAME_CONFIG = require('../core/config');
|
||
|
||
class GameManager {
|
||
constructor(io) {
|
||
this.io = io;
|
||
this.games = {}; // Активные инстансы игр { gameId: GameInstance }
|
||
this.userIdentifierToGameId = {}; // { userId: gameId }
|
||
this.pendingPvPGames = []; // Массив gameId игр, ожидающих второго игрока
|
||
console.log("[GameManager] Initialized.");
|
||
}
|
||
|
||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||
console.log(`[GameManager._removePreviousPendingGames] Called for user: ${identifier}, currentSocket: ${currentSocketId}, excludeGameId: ${excludeGameId}`);
|
||
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||
const gameToRemove = this.games[oldPendingGameId];
|
||
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||
const playerInfo = Object.values(gameToRemove.players).find(p => p.identifier === identifier);
|
||
if (playerInfo && playerInfo.id === GAME_CONFIG.PLAYER_ID) {
|
||
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} (socket: ${currentSocketId}) created/joined new game. Removing their previous owned pending PvP game: ${oldPendingGameId}`);
|
||
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game_creation_or_join');
|
||
} else {
|
||
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} had pending game ${oldPendingGameId}, but was not the primary player. Not removing.`);
|
||
}
|
||
}
|
||
} else {
|
||
console.log(`[GameManager._removePreviousPendingGames] No old pending game found for user ${identifier} or conditions not met.`);
|
||
}
|
||
}
|
||
|
||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
|
||
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey}`);
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
|
||
console.warn(`[GameManager.createGame] User ${identifier} already in game ${this.userIdentifierToGameId[identifier]}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
|
||
|
||
// Если игра существует и НЕ завершена
|
||
if (!existingGame.gameState?.isGameOver) {
|
||
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
|
||
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
|
||
} else {
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
||
}
|
||
this.handleRequestGameState(socket, identifier); // Попытка восстановить сессию в существующей игре
|
||
return;
|
||
} else {
|
||
// Игра существует, но завершена. GameManager должен был ее очистить.
|
||
// Если мы здесь, значит, что-то пошло не так с очисткой.
|
||
console.warn(`[GameManager.createGame] User ${identifier} was mapped to an already finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up stale entry before creating new game.`);
|
||
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_create_for_${identifier}`);
|
||
// this.userIdentifierToGameId[identifier] будет удален в _cleanupGame
|
||
}
|
||
}
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
|
||
const gameId = uuidv4();
|
||
console.log(`[GameManager.createGame] Generated new GameID: ${gameId}`);
|
||
const game = new GameInstance(gameId, this.io, mode, this);
|
||
this.games[gameId] = game;
|
||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||
|
||
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameId;
|
||
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
|
||
const assignedPlayerId = Object.values(game.players).find(p=>p.identifier === identifier)?.id;
|
||
|
||
if (!assignedPlayerId) {
|
||
console.error(`[GameManager.createGame] CRITICAL: Failed to assign player role for user ${identifier} in game ${gameId}.`);
|
||
this._cleanupGame(gameId, 'player_add_failed_no_role_assigned');
|
||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось присвоить роль).' });
|
||
return;
|
||
}
|
||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||
console.log(`[GameManager.createGame] Emitted 'gameCreated' to ${identifier}. gameId: ${gameId}, yourPlayerId: ${assignedPlayerId}`);
|
||
|
||
if (mode === 'ai') {
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) {
|
||
console.log(`[GameManager.createGame] AI game ${gameId} initialized, starting game...`);
|
||
game.startGame();
|
||
} else {
|
||
console.error(`[GameManager.createGame] AI game ${gameId} initialization failed. Cleaning up.`);
|
||
this._cleanupGame(gameId, 'initialization_failed_on_ai_create');
|
||
}
|
||
} else if (mode === 'pvp') {
|
||
if (!this.pendingPvPGames.includes(gameId)) {
|
||
this.pendingPvPGames.push(gameId);
|
||
console.log(`[GameManager.createGame] PvP game ${gameId} added to pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||
}
|
||
game.initializeGame();
|
||
console.log(`[GameManager.createGame] PvP game ${gameId} initialized (or placeholder). Emitting 'waitingForOpponent'.`);
|
||
socket.emit('waitingForOpponent');
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.createGame] game.addPlayer failed for user ${identifier} in game ${gameId}. Cleaning up.`);
|
||
this._cleanupGame(gameId, 'player_add_failed_in_instance');
|
||
}
|
||
}
|
||
|
||
joinGame(socket, gameIdToJoin, identifier) {
|
||
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join GameID: ${gameIdToJoin}`);
|
||
const game = this.games[gameIdToJoin];
|
||
if (!game) {
|
||
console.warn(`[GameManager.joinGame] Game ${gameIdToJoin} not found for user ${identifier}.`);
|
||
socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return;
|
||
}
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.joinGame] User ${identifier} tried to join game ${gameIdToJoin} which is already over.`);
|
||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||
this._cleanupGame(gameIdToJoin, `attempt_to_join_finished_game_${identifier}`);
|
||
return;
|
||
}
|
||
if (game.mode !== 'pvp') {
|
||
console.warn(`[GameManager.joinGame] User ${identifier} tried to join non-PvP game ${gameIdToJoin}. Mode: ${game.mode}`);
|
||
socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP режим).' }); return;
|
||
}
|
||
if (game.playerCount >= 2 && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
|
||
console.warn(`[GameManager.joinGame] User ${identifier} tried to join full PvP game ${gameIdToJoin}. Players: ${game.playerCount}`);
|
||
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||
}
|
||
if (game.ownerIdentifier === identifier && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
|
||
console.warn(`[GameManager.joinGame] User ${identifier} (owner) tried to join their own waiting game ${gameIdToJoin} as a new player.`);
|
||
socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете, как новый игрок.' });
|
||
this.handleRequestGameState(socket, identifier); return;
|
||
}
|
||
if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameIdToJoin) {
|
||
const otherGame = this.games[this.userIdentifierToGameId[identifier]];
|
||
if (otherGame && !otherGame.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.joinGame] User ${identifier} already in another active game: ${this.userIdentifierToGameId[identifier]}. Cannot join ${gameIdToJoin}.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в другой игре. Сначала завершите или покиньте её.' });
|
||
this.handleRequestGameState(socket, identifier); return;
|
||
} else if (otherGame && otherGame.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.joinGame] User ${identifier} was mapped to a finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up before join.`);
|
||
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_join_${identifier}`);
|
||
}
|
||
}
|
||
this._removePreviousPendingGames(socket.id, identifier, gameIdToJoin);
|
||
|
||
if (game.addPlayer(socket, null, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||
console.log(`[GameManager.joinGame] Player ${identifier} successfully added/reconnected to PvP game ${gameIdToJoin}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
|
||
if (game.playerCount === 2) {
|
||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full with 2 active players. Initializing and starting.`);
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) {
|
||
game.startGame();
|
||
} else {
|
||
console.error(`[GameManager.joinGame] PvP game ${gameIdToJoin} initialization failed after 2nd player join. Cleaning up.`);
|
||
this._cleanupGame(gameIdToJoin, 'initialization_failed_on_pvp_join'); return;
|
||
}
|
||
const idx = this.pendingPvPGames.indexOf(gameIdToJoin);
|
||
if (idx > -1) {
|
||
this.pendingPvPGames.splice(idx, 1);
|
||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} removed from pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||
}
|
||
this.broadcastAvailablePvPGames();
|
||
} else {
|
||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} has ${game.playerCount} active players after join/reconnect. Waiting for more or game was already running.`);
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.joinGame] game.addPlayer failed for user ${identifier} trying to join ${gameIdToJoin}.`);
|
||
}
|
||
}
|
||
|
||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||
console.log(`[GameManager.findAndJoinRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
|
||
if (existingGame && !existingGame.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} already in active game: ${this.userIdentifierToGameId[identifier]}.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||
this.handleRequestGameState(socket, identifier); return;
|
||
} else if (existingGame && existingGame.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} mapped to finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up.`);
|
||
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_find_random_${identifier}`);
|
||
}
|
||
}
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
|
||
let gameIdToJoin = null;
|
||
console.log(`[GameManager.findAndJoinRandomPvPGame] Searching pending games for ${identifier}. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||
for (const id of [...this.pendingPvPGames]) { // Iterate over a copy in case of modification
|
||
const pendingGame = this.games[id];
|
||
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
|
||
if (!pendingGame.gameState || !pendingGame.gameState.isGameOver) {
|
||
gameIdToJoin = id;
|
||
console.log(`[GameManager.findAndJoinRandomPvPGame] Found suitable pending game: ${gameIdToJoin} for user ${identifier}.`);
|
||
break;
|
||
} else {
|
||
console.log(`[GameManager.findAndJoinRandomPvPGame] Pending game ${id} is already over. Skipping and cleaning.`);
|
||
this._cleanupGame(id, `stale_finished_pending_game_during_find_random`); // Clean up stale finished game
|
||
}
|
||
}
|
||
}
|
||
if (gameIdToJoin) {
|
||
this.joinGame(socket, gameIdToJoin, identifier);
|
||
} else {
|
||
console.log(`[GameManager.findAndJoinRandomPvPGame] No suitable pending game found for ${identifier}. Creating a new PvP game.`);
|
||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||
}
|
||
}
|
||
|
||
handlePlayerAction(identifier, actionData) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, AbilityID: ${actionData?.abilityId}, GameID from map: ${gameId}`);
|
||
const game = this.games[gameId];
|
||
if (game) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} in game ${gameId} attempted action, but game is ALREADY OVER. Action ignored.`);
|
||
game.playerSockets[identifier]?.socket.emit('gameError', {message: "Действие невозможно: игра уже завершена."});
|
||
// Potentially send gameNotFound or re-send gameOver if client missed it
|
||
this.handleRequestGameState(game.playerSockets[identifier]?.socket || this._findClientSocketByIdentifier(identifier), identifier);
|
||
return;
|
||
}
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
if (playerInfo && playerInfo.socket && playerInfo.socket.connected && !playerInfo.isTemporarilyDisconnected) {
|
||
console.log(`[GameManager.handlePlayerAction] Forwarding action from user ${identifier} (Socket: ${playerInfo.socket.id}) to game ${gameId}.`);
|
||
game.processPlayerAction(playerInfo.socket.id, actionData);
|
||
} else if (playerInfo && playerInfo.isTemporarilyDisconnected) {
|
||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} (Socket: ${playerInfo.socket?.id}) in game ${gameId} attempted action, but is temporarily disconnected. Action ignored.`);
|
||
playerInfo.socket?.emit('gameError', {message: "Действие невозможно: вы временно отключены."});
|
||
} else if (playerInfo && playerInfo.socket && !playerInfo.socket.connected) {
|
||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} (Socket: ${playerInfo.socket.id}) in game ${gameId} attempted action, but socket is reported as disconnected by server. Action ignored. Potential state mismatch.`);
|
||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} attempted action for game ${gameId}, but active player info or socket not found in game instance. Removing from user map.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока при действии).' });
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.handlePlayerAction] User ${identifier} attempted action, but game ${gameId} (from map) not found in this.games. Removing from user map.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена (игра отсутствует).' });
|
||
}
|
||
}
|
||
|
||
handlePlayerSurrender(identifier) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`);
|
||
|
||
const game = this.games[gameId];
|
||
|
||
if (game) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} tried to surrender in game ${gameId} which is ALREADY OVER. Ignoring.`);
|
||
// _cleanupGame should have already run or will run.
|
||
// Ensure map is clear if it's somehow stale.
|
||
if (this.userIdentifierToGameId[identifier] === gameId) {
|
||
console.warn(`[GameManager.handlePlayerSurrender] Stale map entry for ${identifier} to finished game ${gameId}. Cleaning.`);
|
||
delete this.userIdentifierToGameId[identifier]; // Direct cleanup if game is confirmed over.
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (typeof game.playerDidSurrender === 'function') {
|
||
console.log(`[GameManager.handlePlayerSurrender] Forwarding surrender from user ${identifier} to game ${gameId}.`);
|
||
game.playerDidSurrender(identifier); // This method will call _cleanupGame internally
|
||
} else {
|
||
console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} is missing playerDidSurrender method! Attempting fallback cleanup for PvP.`);
|
||
if (game.mode === 'pvp' && game.gameState && !game.gameState.isGameOver) {
|
||
const surrenderedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
if (surrenderedPlayerInfo) {
|
||
const opponentInfo = Object.values(game.players).find(p => p.identifier !== identifier && !p.isTemporarilyDisconnected);
|
||
const winnerRole = opponentInfo ? opponentInfo.id : null;
|
||
if (typeof game.endGameDueToDisconnect === 'function') {
|
||
game.endGameDueToDisconnect(surrenderedPlayerInfo.id, surrenderedPlayerInfo.chosenCharacterKey, "opponent_surrendered_fallback", winnerRole);
|
||
} else {
|
||
this._cleanupGame(gameId, "surrender_fallback_cleanup_missing_end_method");
|
||
}
|
||
} else {
|
||
this._cleanupGame(gameId, "surrender_player_not_found_in_game_instance_fallback");
|
||
}
|
||
} else if (game.mode === 'ai') {
|
||
console.log(`[GameManager.handlePlayerSurrender] User ${identifier} in AI game ${gameId} surrendered. No playerDidSurrender. Disconnect will handle.`);
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} surrendered, but game ${gameId} (from map) not found in this.games. User map might be stale or already cleaned up.`);
|
||
if (this.userIdentifierToGameId[identifier] === gameId || this.userIdentifierToGameId[identifier]) {
|
||
console.log(`[GameManager.handlePlayerSurrender] Clearing map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]}.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
|
||
_findClientSocketByIdentifier(identifier) {
|
||
for (const sid of this.io.sockets.sockets.keys()) {
|
||
const s = this.io.sockets.sockets.get(sid);
|
||
if (s && s.userData && s.userData.userId === identifier && s.connected) {
|
||
// console.log(`[GameManager._findClientSocketByIdentifier] Found active socket ${s.id} for identifier ${identifier}.`);
|
||
return s;
|
||
}
|
||
}
|
||
// console.log(`[GameManager._findClientSocketByIdentifier] No active socket found for identifier ${identifier}.`);
|
||
return null;
|
||
}
|
||
|
||
handleDisconnect(socketId, identifier) {
|
||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, User: ${identifier}, GameID from map: ${gameIdFromMap}`);
|
||
|
||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||
|
||
if (game) {
|
||
if (game.gameState && game.gameState.isGameOver) {
|
||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket: ${socketId}) is ALREADY OVER. Disconnect processing skipped for game logic.`);
|
||
// _cleanupGame (called by playerDidSurrender or other end game methods) is responsible for clearing userIdentifierToGameId.
|
||
// If the map entry still exists, it implies _cleanupGame might not have completed or there's a race condition.
|
||
// However, we shouldn't initiate new game logic like handlePlayerPotentiallyLeft.
|
||
return;
|
||
}
|
||
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} found (and not game over). PlayerInfo for user ${identifier}: ${playerInfo ? `Role: ${playerInfo.id}, CurrentSocketInGame: ${playerInfo.socket?.id}, IsTempDisconnected: ${playerInfo.isTemporarilyDisconnected}` : 'Not found in game instance'}`);
|
||
|
||
if (playerInfo && playerInfo.socket && playerInfo.socket.id === socketId && !playerInfo.isTemporarilyDisconnected) {
|
||
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active socket for user ${identifier} (Role: ${playerInfo.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
|
||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
|
||
} else {
|
||
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} is missing handlePlayerPotentiallyLeft method!`);
|
||
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_in_instance_on_disconnect");
|
||
}
|
||
} else if (playerInfo && playerInfo.socket && playerInfo.socket.id !== socketId) {
|
||
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier} (active socket in game ${gameIdFromMap} is ${playerInfo.socket.id}). Ignoring this disconnect for game logic.`);
|
||
} else if (playerInfo && playerInfo.isTemporarilyDisconnected && playerInfo.socket?.id === socketId) {
|
||
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already being temporarily disconnected. Reconnect timer should handle final cleanup.`);
|
||
} else if (!playerInfo && gameIdFromMap) {
|
||
console.log(`[GameManager.handleDisconnect] User ${identifier} was mapped to game ${gameIdFromMap}, but not found in game.players.`);
|
||
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
|
||
console.warn(`[GameManager.handleDisconnect] Removing stale map entry for ${identifier} to game ${gameIdFromMap} where player was not found in instance.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
} else {
|
||
console.log(`[GameManager.handleDisconnect] User ${identifier} (Socket: ${socketId}) disconnected, but no active game instance found for gameId ${gameIdFromMap} (or gameId was undefined).`);
|
||
if (this.userIdentifierToGameId[identifier]) { // If a mapping exists, even if gameIdFromMap was undefined or game not in this.games
|
||
console.warn(`[GameManager.handleDisconnect] Removing map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]}.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
|
||
_cleanupGame(gameId, reason = 'unknown') {
|
||
console.log(`[GameManager._cleanupGame] Attempting to cleanup GameID: ${gameId}, Reason: ${reason}`);
|
||
const game = this.games[gameId];
|
||
if (!game) {
|
||
console.warn(`[GameManager._cleanupGame] Game ${gameId} not found in this.games. Checking pending list and user map.`);
|
||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||
if (pendingIdx > -1) {
|
||
this.pendingPvPGames.splice(pendingIdx, 1);
|
||
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pending list (instance was already gone). Reason: ${reason}. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
// Ensure any lingering user map entries for this non-existent game are cleared.
|
||
let mapCleaned = false;
|
||
for (const idKey in this.userIdentifierToGameId) {
|
||
if (this.userIdentifierToGameId[idKey] === gameId) {
|
||
console.log(`[GameManager._cleanupGame] Clearing STALE mapping for user ${idKey} to non-existent game ${gameId}.`);
|
||
delete this.userIdentifierToGameId[idKey];
|
||
mapCleaned = true;
|
||
}
|
||
}
|
||
if (mapCleaned) console.log(`[GameManager._cleanupGame] Stale user maps cleared for game ${gameId}.`);
|
||
return false; // Game was not found in this.games
|
||
}
|
||
|
||
console.log(`[GameManager._cleanupGame] Cleaning up game ${gameId}. Owner: ${game.ownerIdentifier}. Reason: ${reason}.`);
|
||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||
if (typeof game.clearAllReconnectTimers === 'function') {
|
||
game.clearAllReconnectTimers();
|
||
console.log(`[GameManager._cleanupGame] Called clearAllReconnectTimers for game ${gameId}.`);
|
||
}
|
||
|
||
// Ensure gameState is marked as over if not already
|
||
if (game.gameState && !game.gameState.isGameOver) {
|
||
console.warn(`[GameManager._cleanupGame] Game ${gameId} was not marked as 'isGameOver' during cleanup. Marking now. Reason: ${reason}`);
|
||
game.gameState.isGameOver = true;
|
||
}
|
||
|
||
// Remove all players of this game from the global userIdentifierToGameId map
|
||
let playersInGameCleaned = 0;
|
||
Object.values(game.players).forEach(pInfo => {
|
||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
||
console.log(`[GameManager._cleanupGame] Deleting mapping for player ${pInfo.identifier} (Role: ${pInfo.id}) from game ${gameId}.`);
|
||
delete this.userIdentifierToGameId[pInfo.identifier];
|
||
playersInGameCleaned++;
|
||
}
|
||
});
|
||
// Also check the owner, in case they weren't in game.players (e.g., game created but owner disconnected before fully joining)
|
||
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
|
||
if (!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Only if not already cleaned
|
||
console.log(`[GameManager._cleanupGame] Deleting mapping for owner ${game.ownerIdentifier} from game ${gameId} (was not in game.players or already cleaned).`);
|
||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
||
playersInGameCleaned++;
|
||
}
|
||
}
|
||
if (playersInGameCleaned > 0) {
|
||
console.log(`[GameManager._cleanupGame] ${playersInGameCleaned} player mappings cleared for game ${gameId}.`);
|
||
}
|
||
|
||
|
||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||
if (pendingIdx > -1) {
|
||
this.pendingPvPGames.splice(pendingIdx, 1);
|
||
console.log(`[GameManager._cleanupGame] Game ${gameId} removed from pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
|
||
}
|
||
|
||
delete this.games[gameId];
|
||
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||
this.broadcastAvailablePvPGames(); // Update list for all clients
|
||
return true;
|
||
}
|
||
|
||
getAvailablePvPGamesListForClient() {
|
||
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Generating list from pending: ${this.pendingPvPGames.join(', ')}`);
|
||
return this.pendingPvPGames
|
||
.map(gameId => {
|
||
const game = this.games[gameId];
|
||
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||
const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||
let p1Username = 'Игрок', p1CharName = 'Неизвестный';
|
||
const ownerId = game.ownerIdentifier;
|
||
|
||
if (p1Info && p1Info.socket && p1Info.socket.userData) { // Check for userData
|
||
p1Username = p1Info.socket.userData.username || `User#${String(p1Info.identifier).substring(0,4)}`;
|
||
const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey);
|
||
p1CharName = charData?.name || p1Info.chosenCharacterKey || 'Не выбран';
|
||
} else if (ownerId){
|
||
// console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is pending, p1Info not found or no socket/userData. Using owner info. Owner: ${ownerId}`);
|
||
// Try to find owner's socket if they are still connected to the server (even if not in this game's player list actively)
|
||
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
||
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
||
const ownerCharKey = game.playerCharacterKey; // This should be the char key of the first player/owner
|
||
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
||
}
|
||
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} - Owner: ${ownerId}, P1 Username: ${p1Username}, Char: ${p1CharName}`);
|
||
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
||
}
|
||
return null;
|
||
})
|
||
.filter(info => info !== null);
|
||
}
|
||
|
||
broadcastAvailablePvPGames() {
|
||
const list = this.getAvailablePvPGamesListForClient();
|
||
// console.log(`[GameManager.broadcastAvailablePvPGames] Broadcasting list of ${list.length} games.`);
|
||
this.io.emit('availablePvPGamesList', list);
|
||
}
|
||
|
||
handleRequestGameState(socket, identifier) {
|
||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handleRequestGameState] User: ${identifier} (New Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
|
||
|
||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||
|
||
if (game) {
|
||
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found for user ${identifier}. Game mode: ${game.mode}, Active players in instance: ${game.playerCount}, Total player entries: ${Object.keys(game.players).length}`);
|
||
const playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier);
|
||
console.log(`[GameManager.handleRequestGameState] PlayerInfo for user ${identifier} in game ${gameIdFromMap}: ${playerInfoInGameInstance ? `Role: ${playerInfoInGameInstance.id}, OldSocketInGame: ${playerInfoInGameInstance.socket?.id}, TempDisco: ${playerInfoInGameInstance.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
|
||
|
||
if (playerInfoInGameInstance) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} for user ${identifier} IS ALREADY OVER. Emitting 'gameNotFound'. Cleanup should handle map.`);
|
||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
||
// _cleanupGame is responsible for clearing userIdentifierToGameId when a game ends.
|
||
// If the game is over, but the user is still mapped, _cleanupGame might not have run or completed fully.
|
||
// We don't call _cleanupGame here directly, as it might be called by the game ending logic itself.
|
||
return;
|
||
}
|
||
|
||
console.log(`[GameManager.handleRequestGameState] Restoring game ${gameIdFromMap} for user ${identifier}. NewSocket: ${socket.id}. OldSocketInGame: ${playerInfoInGameInstance.socket?.id}. Player role: ${playerInfoInGameInstance.id}`);
|
||
|
||
if (typeof game.handlePlayerReconnected === 'function') {
|
||
const reconnected = game.handlePlayerReconnected(playerInfoInGameInstance.id, socket);
|
||
console.log(`[GameManager.handleRequestGameState] Called game.handlePlayerReconnected for role ${playerInfoInGameInstance.id}. Result: ${reconnected}`);
|
||
if (!reconnected && game.gameState && !game.gameState.isGameOver) { // if reconnected failed but game is active
|
||
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected returned false for user ${identifier}. This might indicate an issue.`);
|
||
// It could be that the player wasn't marked as disconnected, or an error occurred.
|
||
// The client might still expect game state.
|
||
} else if (!reconnected && game.gameState?.isGameOver) {
|
||
// If reconnect failed AND game is over, ensure client gets gameNotFound.
|
||
socket.emit('gameNotFound', { message: 'Не удалось восстановить сессию: игра уже завершена.' });
|
||
return;
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} is missing handlePlayerReconnected method! Attempting fallback socket update.`);
|
||
const oldSocketId = playerInfoInGameInstance.socket?.id;
|
||
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
||
delete game.players[oldSocketId];
|
||
}
|
||
playerInfoInGameInstance.socket = socket;
|
||
game.players[socket.id] = playerInfoInGameInstance;
|
||
if (!game.playerSockets[playerInfoInGameInstance.id] || game.playerSockets[playerInfoInGameInstance.id].id !== socket.id) {
|
||
game.playerSockets[playerInfoInGameInstance.id] = socket;
|
||
}
|
||
}
|
||
|
||
// If handlePlayerReconnected returned false but game is not over, we might still need to send state.
|
||
// If game became game over inside handlePlayerReconnected, it should have emitted gameOver.
|
||
|
||
// Re-check game over state as handlePlayerReconnected might have changed it (e.g. if opponent didn't reconnect and game ended)
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} became game over during reconnect logic for ${identifier}. Emitting 'gameNotFound'.`);
|
||
socket.emit('gameNotFound', { message: 'Игра завершилась во время попытки переподключения.' });
|
||
return;
|
||
}
|
||
|
||
|
||
socket.join(game.id);
|
||
console.log(`[GameManager.handleRequestGameState] New socket ${socket.id} for user ${identifier} joined room ${game.id}.`);
|
||
|
||
const pCharKey = playerInfoInGameInstance.chosenCharacterKey;
|
||
const pData = dataUtils.getCharacterData(pCharKey);
|
||
const opponentRole = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const oCharKeyFromGameState = game.gameState?.[opponentRole]?.characterKey;
|
||
const oCharKeyFromInstance = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey;
|
||
const oCharKey = oCharKeyFromGameState || oCharKeyFromInstance;
|
||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||
|
||
console.log(`[GameManager.handleRequestGameState] User's charKey: ${pCharKey}. Opponent's role: ${opponentRole}, charKey: ${oCharKey || 'None (pending/AI placeholder)'}.`);
|
||
|
||
if (pData && (oData || (game.mode === 'pvp' && game.playerCount === 1 && !oCharKey) || game.mode === 'ai') && game.gameState) {
|
||
const gameStateToSend = game.gameState;
|
||
const logBufferToSend = game.consumeLogBuffer();
|
||
console.log(`[GameManager.handleRequestGameState] Emitting 'gameStarted' (for restore) to ${identifier} for game ${game.id}. Game state isGameOver: ${gameStateToSend.isGameOver}. Log entries: ${logBufferToSend.length}`);
|
||
|
||
socket.emit('gameStarted', {
|
||
gameId: game.id,
|
||
yourPlayerId: playerInfoInGameInstance.id,
|
||
initialGameState: gameStateToSend,
|
||
playerBaseStats: pData.baseStats,
|
||
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||
playerAbilities: pData.abilities,
|
||
opponentAbilities: oData?.abilities || [],
|
||
log: logBufferToSend,
|
||
clientConfig: { ...GAME_CONFIG }
|
||
});
|
||
|
||
if (game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier && !game.gameState.isGameOver) {
|
||
console.log(`[GameManager.handleRequestGameState] PvP game ${game.id} is still pending for owner ${identifier}. Emitting 'waitingForOpponent'.`);
|
||
socket.emit('waitingForOpponent');
|
||
}
|
||
|
||
if (!game.gameState.isGameOver && typeof game.turnTimer?.start === 'function' && !game.isGameEffectivelyPaused()) {
|
||
const isAiTurnForTimer = game.mode === 'ai' && !game.gameState.isPlayerTurn && game.gameState.opponent?.characterKey !== null;
|
||
console.log(`[GameManager.handleRequestGameState] Restarting turn timer for game ${game.id}. isPlayerTurn: ${game.gameState.isPlayerTurn}, isAiTurnForTimer: ${isAiTurnForTimer}`);
|
||
game.turnTimer.start(game.gameState.isPlayerTurn, isAiTurnForTimer);
|
||
} else if (game.gameState.isGameOver) {
|
||
console.log(`[GameManager.handleRequestGameState] Game ${game.id} is already over, no timer restart.`);
|
||
} else if (game.isGameEffectivelyPaused()){
|
||
console.log(`[GameManager.handleRequestGameState] Game ${game.id} is effectively paused, turn timer not started.`);
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.handleRequestGameState] Data load failed for game ${game.id} / user ${identifier} on reconnect. pData: ${!!pData}, oData: ${!!oData} (oCharKey: ${oCharKey}), gameState: ${!!game.gameState}`);
|
||
this._handleGameRecoveryError(socket, game.id, identifier, 'data_load_fail_reconnect_manager');
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.handleRequestGameState] User ${identifier} was mapped to game ${gameIdFromMap}, but NOT found in game.players. This indicates a serious state inconsistency.`);
|
||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_instance_reconnect_manager');
|
||
}
|
||
} else {
|
||
console.log(`[GameManager.handleRequestGameState] No active game session found for user ${identifier} (GameID from map was ${gameIdFromMap || 'undefined'}). Emitting 'gameNotFound'.`);
|
||
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
||
// Ensure map is clear if it's somehow stale
|
||
if (this.userIdentifierToGameId[identifier]) {
|
||
console.warn(`[GameManager.handleRequestGameState] Clearing stale map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]} but game not found.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
|
||
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
||
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||
if (gameId) {
|
||
// Attempt to cleanup the problematic game
|
||
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
|
||
} else if (this.userIdentifierToGameId[identifier]) {
|
||
// If gameId was null, but user was still mapped, cleanup the mapped game
|
||
const problematicGameId = this.userIdentifierToGameId[identifier];
|
||
console.warn(`[GameManager._handleGameRecoveryError] GameId was null/undefined for user ${identifier}, but they were in map to game ${problematicGameId}. Attempting cleanup of ${problematicGameId}.`);
|
||
this._cleanupGame(problematicGameId, `recovery_error_null_gameid_for_${identifier}_reason_${reasonCode}`);
|
||
}
|
||
// This will also clear userIdentifierToGameId[identifier] if _cleanupGame didn't.
|
||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
||
}
|
||
}
|
||
|
||
module.exports = GameManager; |