// /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 ожидающих PvP игр console.log("[GameManager] Инициализирован."); } _cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') { const oldPendingGameId = this.userIdentifierToGameId[identifier]; if (oldPendingGameId && this.games[oldPendingGameId]) { const gameToRemove = this.games[oldPendingGameId]; // Убеждаемся, что это именно ожидающая PvP игра этого пользователя if (gameToRemove.mode === 'pvp' && gameToRemove.ownerIdentifier === identifier && // Он владелец gameToRemove.playerCount === 1 && // В игре только он this.pendingPvPGames.includes(oldPendingGameId) && // Игра в списке ожидающих (!gameToRemove.gameState || !gameToRemove.gameState.isGameOver) // И она не завершена ) { console.log(`[GameManager._cleanupPreviousPendingGameForUser] Пользователь ${identifier} имеет существующую ожидающую PvP игру ${oldPendingGameId}. Удаление. Причина: ${reasonSuffix}`); this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_pvp_game_${reasonSuffix}`); return true; } } return false; } createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) { console.log(`[GameManager.createGame] Пользователь: ${identifier} (Socket: ${socket.id}), Режим: ${mode}, Персонаж: ${chosenCharacterKey || 'По умолчанию'}`); const existingGameIdForUser = this.userIdentifierToGameId[identifier]; if (existingGameIdForUser && this.games[existingGameIdForUser]) { const existingGame = this.games[existingGameIdForUser]; if (existingGame.gameState && existingGame.gameState.isGameOver) { console.warn(`[GameManager.createGame] Пользователь ${identifier} был в завершенной игре ${existingGameIdForUser}. Очистка перед созданием новой.`); this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`); } else { const isHisOwnPendingPvp = existingGame.mode === 'pvp' && existingGame.ownerIdentifier === identifier && existingGame.playerCount === 1 && this.pendingPvPGames.includes(existingGameIdForUser); if (!isHisOwnPendingPvp) { console.warn(`[GameManager.createGame] Пользователь ${identifier} уже в активной игре ${existingGameIdForUser} (режим: ${existingGame.mode}, владелец: ${existingGame.ownerIdentifier}). Невозможно создать новую.`); socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' }); this.handleRequestGameState(socket, identifier); return; } } } this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`); console.log(`[GameManager.createGame] После возможной очистки, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`); const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier]; if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) { console.error(`[GameManager.createGame] КРИТИЧЕСКАЯ ОШИБКА ЛОГИКИ: Пользователь ${identifier} все еще сопоставлен с активной игрой ${stillExistingGameIdAfterCleanup} после попытки очистки. Создание отклонено.`); socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию.' }); this.handleRequestGameState(socket, identifier); return; } const gameId = uuidv4(); console.log(`[GameManager.createGame] Новый GameID: ${gameId}`); const game = new GameInstance(gameId, this.io, mode, this); this.games[gameId] = game; const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena'); if (game.addPlayer(socket, charKeyForPlayer, identifier)) { this.userIdentifierToGameId[identifier] = gameId; const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); const assignedPlayerId = playerInfo?.id; const actualCharacterKey = playerInfo?.chosenCharacterKey; if (!assignedPlayerId || !actualCharacterKey) { console.error(`[GameManager.createGame] КРИТИЧЕСКИ: Не удалось получить роль/ключ персонажа после addPlayer для ${identifier} в игре ${gameId}. Очистка.`); this._cleanupGame(gameId, 'player_info_missing_after_add_on_create'); socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' }); return; } console.log(`[GameManager.createGame] Игрок ${identifier} добавлен в игру ${gameId} как ${assignedPlayerId}. Карта пользователя обновлена. Текущая карта для ${identifier}: ${this.userIdentifierToGameId[identifier]}`); socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId, chosenCharacterKey: actualCharacterKey }); if (mode === 'ai') { if (game.initializeGame()) { console.log(`[GameManager.createGame] AI игра ${gameId} инициализирована GameManager, запуск...`); game.startGame(); } else { console.error(`[GameManager.createGame] Инициализация AI игры ${gameId} не удалась в GameManager. Очистка.`); this._cleanupGame(gameId, 'init_fail_ai_create_gm'); } } else if (mode === 'pvp') { if (game.initializeGame()) { if (!this.pendingPvPGames.includes(gameId)) { this.pendingPvPGames.push(gameId); } socket.emit('waitingForOpponent'); this.broadcastAvailablePvPGames(); } else { console.error(`[GameManager.createGame] Инициализация PvP игры ${gameId} (один игрок) не удалась. Очистка.`); this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player'); } } } else { console.error(`[GameManager.createGame] game.addPlayer не удалось для ${identifier} в ${gameId}. Очистка.`); this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create'); } } joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) { console.log(`[GameManager.joinGame] Пользователь: ${identifier} (Socket: ${socket.id}) пытается присоединиться к ${gameIdToJoin} с персонажем ${chosenCharacterKey || 'По умолчанию'}`); const gameToJoin = this.games[gameIdToJoin]; if (!gameToJoin) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; } if (gameToJoin.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_game_${identifier}`); return; } if (gameToJoin.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; } const playerInfoInTargetGame = Object.values(gameToJoin.players).find(p => p.identifier === identifier); if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } // Запрещаем владельцу "присоединяться" к своей ожидающей игре как новый игрок, если он не был временно отключен. // Если он хочет вернуться, он должен использовать requestGameState. if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) { console.warn(`[GameManager.joinGame] Пользователь ${identifier} пытается присоединиться к своей игре ${gameIdToJoin}, где он владелец и не отключен. Обработка как запрос на переподключение.`); this.handleRequestGameState(socket, identifier); return; } const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier]; if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) { console.warn(`[GameManager.joinGame] Пользователь ${identifier} был в завершенной игре ${currentActiveGameIdUserIsIn} при попытке присоединиться к ${gameIdToJoin}. Очистка старой.`); this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`); } const stillExistingGameIdForUser = this.userIdentifierToGameId[identifier]; if (stillExistingGameIdForUser && stillExistingGameIdForUser !== gameIdToJoin && this.games[stillExistingGameIdForUser] && !this.games[stillExistingGameIdForUser].gameState?.isGameOver) { const usersCurrentGame = this.games[stillExistingGameIdForUser]; const isHisOwnPendingPvp = usersCurrentGame.mode === 'pvp' && usersCurrentGame.ownerIdentifier === identifier && usersCurrentGame.playerCount === 1 && this.pendingPvPGames.includes(stillExistingGameIdForUser); if (isHisOwnPendingPvp) { console.log(`[GameManager.joinGame] Пользователь ${identifier} является владельцем ожидающей игры ${stillExistingGameIdForUser}, но хочет присоединиться к ${gameIdToJoin}. Очистка старой игры.`); this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`); } else { console.warn(`[GameManager.joinGame] Пользователь ${identifier} находится в другой активной игре ${stillExistingGameIdForUser}. Невозможно присоединиться к ${gameIdToJoin}.`); socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' }); this.handleRequestGameState(socket, identifier); return; } } console.log(`[GameManager.joinGame] После возможной очистки перед присоединением, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`); const charKeyForJoin = chosenCharacterKey || 'elena'; if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) { this.userIdentifierToGameId[identifier] = gameIdToJoin; const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier); if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) { console.error(`[GameManager.joinGame] КРИТИЧЕСКИ: Не удалось получить роль/ключ персонажа после addPlayer для ${identifier}, присоединяющегося к ${gameIdToJoin}.`); socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' }); if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier]; return; } console.log(`[GameManager.joinGame] Игрок ${identifier} добавлен/переподключен к ${gameIdToJoin} как ${joinedPlayerInfo.id}. Карта пользователя обновлена. Текущая карта для ${identifier}: ${this.userIdentifierToGameId[identifier]}`); socket.emit('gameCreated', { gameId: gameIdToJoin, mode: gameToJoin.mode, yourPlayerId: joinedPlayerInfo.id, chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey }); if (gameToJoin.playerCount === 2) { console.log(`[GameManager.joinGame] Игра ${gameIdToJoin} теперь заполнена. Инициализация и запуск.`); // Важно! Инициализация может обновить ключи персонажей, если они были одинаковыми. if (gameToJoin.initializeGame()) { gameToJoin.startGame(); } else { this._cleanupGame(gameIdToJoin, 'full_init_fail_pvp_join_gm'); return; } const idx = this.pendingPvPGames.indexOf(gameIdToJoin); if (idx > -1) this.pendingPvPGames.splice(idx, 1); this.broadcastAvailablePvPGames(); } } else { console.warn(`[GameManager.joinGame] gameToJoin.addPlayer вернул false для пользователя ${identifier} в игре ${gameIdToJoin}.`); } } findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { console.log(`[GameManager.findRandomPvPGame] Пользователь: ${identifier} (Socket: ${socket.id}), Персонаж для создания: ${chosenCharacterKeyForCreation}`); const existingGameIdForUser = this.userIdentifierToGameId[identifier]; if (existingGameIdForUser && this.games[existingGameIdForUser]) { const existingGame = this.games[existingGameIdForUser]; if (existingGame.gameState && existingGame.gameState.isGameOver) { console.warn(`[GameManager.findRandomPvPGame] Пользователь ${identifier} был в завершенной игре ${existingGameIdForUser}. Очистка.`); this._cleanupGame(existingGameIdForUser, `stale_finished_on_find_random_${identifier}`); } else { console.warn(`[GameManager.findRandomPvPGame] Пользователь ${identifier} уже в активной/ожидающей игре ${existingGameIdForUser}. Невозможно найти случайную.`); socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } } this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`); console.log(`[GameManager.findRandomPvPGame] После возможной очистки, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`); const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier]; if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) { console.error(`[GameManager.findRandomPvPGame] КРИТИЧЕСКАЯ ОШИБКА ЛОГИКИ: Пользователь ${identifier} все еще сопоставлен с активной игрой ${stillExistingGameIdAfterCleanup} после попытки очистки. Поиск случайной игры отклонен.`); socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию для поиска.' }); this.handleRequestGameState(socket, identifier); return; } let gameIdToJoin = null; for (const id of [...this.pendingPvPGames]) { const pendingGame = this.games[id]; if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier && (!pendingGame.gameState || !pendingGame.gameState.isGameOver)) { gameIdToJoin = id; break; } else if (!pendingGame || (pendingGame?.gameState && pendingGame.gameState.isGameOver)) { console.warn(`[GameManager.findRandomPvPGame] Найдена устаревшая/завершенная ожидающая игра ${id}. Очистка.`); this._cleanupGame(id, `stale_finished_pending_on_find_random`); } } if (gameIdToJoin) { console.log(`[GameManager.findRandomPvPGame] Найдена ожидающая игра ${gameIdToJoin} для ${identifier}. Присоединение...`); const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)]; this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey); } else { console.log(`[GameManager.findRandomPvPGame] Подходящая ожидающая игра не найдена. Создание новой PvP игры для ${identifier}.`); this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); } } handlePlayerAction(identifier, actionData) { const gameId = this.userIdentifierToGameId[identifier]; const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket; if (playerSocket) { console.warn(`[GameManager.handlePlayerAction] Действие от ${identifier} для игры ${gameId}, но игра завершена. Запрос состояния.`); this.handleRequestGameState(playerSocket, identifier); } else { console.warn(`[GameManager.handlePlayerAction] Действие от ${identifier} для игры ${gameId}, игра завершена, но сокет для пользователя не найден.`); this._cleanupGame(gameId, `action_on_over_no_socket_gm_${identifier}`); } return; } game.processPlayerAction(identifier, actionData); } else { console.warn(`[GameManager.handlePlayerAction] Игра для пользователя ${identifier} не найдена (сопоставлена с игрой ${gameId}). Очистка записи в карте.`); 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] Пользователь: ${identifier} сдался. GameID из карты: ${gameId}`); const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { console.warn(`[GameManager.handlePlayerSurrender] Пользователь ${identifier} в игре ${gameId} сдается, но игра УЖЕ ЗАВЕРШЕНА.`); return; } if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier); else { console.error(`[GameManager.handlePlayerSurrender] КРИТИЧЕСКИ: GameInstance ${gameId} отсутствует playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); } } else { console.warn(`[GameManager.handlePlayerSurrender] Игра для пользователя ${identifier} не найдена. Очистка записи в карте.`); if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier]; } } handleLeaveAiGame(identifier) { const gameId = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handleLeaveAiGame] Пользователь: ${identifier} покидает AI игру. GameID из карты: ${gameId}`); const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { console.warn(`[GameManager.handleLeaveAiGame] Пользователь ${identifier} в игре ${gameId} выходит, но игра УЖЕ ЗАВЕРШЕНА.`); return; } if (game.mode === 'ai') { if (typeof game.playerExplicitlyLeftAiGame === 'function') { game.playerExplicitlyLeftAiGame(identifier); } else { console.error(`[GameManager.handleLeaveAiGame] КРИТИЧЕСКИ: GameInstance ${gameId} отсутствует playerExplicitlyLeftAiGame! Прямая очистка.`); this._cleanupGame(gameId, "leave_ai_missing_method_gm"); } } else { console.warn(`[GameManager.handleLeaveAiGame] Пользователь ${identifier} отправил leaveAiGame, но игра ${gameId} не в режиме AI (${game.mode}).`); const clientSocket = this._findClientSocketByIdentifier(identifier); if(clientSocket) clientSocket.emit('gameError', { message: 'Вы не в AI игре.' }); } } else { console.warn(`[GameManager.handleLeaveAiGame] Игра для пользователя ${identifier} не найдена. Очистка записи в карте.`); if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier]; const clientSocket = this._findClientSocketByIdentifier(identifier); if(clientSocket) clientSocket.emit('gameNotFound', { message: 'AI игра не найдена для выхода.' }); } } _findClientSocketByIdentifier(identifier) { for (const s of this.io.sockets.sockets.values()) { if (s && s.userData && s.userData.userId === identifier && s.connected) return s; } return null; } handleDisconnect(socketId, identifier) { const gameIdFromMap = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, Пользователь: ${identifier}, GameID из карты: ${gameIdFromMap}`); const game = gameIdFromMap ? this.games[gameIdFromMap] : null; if (game) { if (game.gameState?.isGameOver) { console.log(`[GameManager.handleDisconnect] Игра ${gameIdFromMap} для пользователя ${identifier} (сокет ${socketId}) УЖЕ ЗАВЕРШЕНА. Игра будет очищена своей собственной логикой или следующим релевантным действием.`); return; } const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfoInGame) { // Игрок существует в этой игре console.log(`[GameManager.handleDisconnect] Отключающийся сокет ${socketId} для пользователя ${identifier} (Роль: ${playerInfoInGame.id}) в игре ${gameIdFromMap}. Уведомление GameInstance.`); if (typeof game.handlePlayerPotentiallyLeft === 'function') { // Передаем фактический socketId, который отключился. PCH определит, устарел ли он. game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey, socketId); } else { console.error(`[GameManager.handleDisconnect] КРИТИЧЕСКИ: GameInstance ${gameIdFromMap} отсутствует handlePlayerPotentiallyLeft!`); this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm"); } } else { console.warn(`[GameManager.handleDisconnect] Пользователь ${identifier} сопоставлен с игрой ${gameIdFromMap}, но не найден в game.players. Это может указывать на устаревшую запись userIdentifierToGameId. Очистка карты для этого пользователя.`); if (this.userIdentifierToGameId[identifier] === gameIdFromMap) { delete this.userIdentifierToGameId[identifier]; } } } else { if (this.userIdentifierToGameId[identifier]) { console.warn(`[GameManager.handleDisconnect] Экземпляр игры для gameId ${gameIdFromMap} (пользователь ${identifier}) не найден. Очистка устаревшей записи в карте.`); delete this.userIdentifierToGameId[identifier]; } } } _cleanupGame(gameId, reason = 'unknown') { console.log(`[GameManager._cleanupGame] Попытка очистки для GameID: ${gameId}, Причина: ${reason}`); const game = this.games[gameId]; if (!game) { console.warn(`[GameManager._cleanupGame] Экземпляр игры для ${gameId} не найден в this.games. Очистка связанных записей.`); const pendingIdx = this.pendingPvPGames.indexOf(gameId); if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); console.log(`[GameManager._cleanupGame] ${gameId} удален из pendingPvPGames.`); } Object.keys(this.userIdentifierToGameId).forEach(idKey => { if (this.userIdentifierToGameId[idKey] === gameId) { delete this.userIdentifierToGameId[idKey]; console.log(`[GameManager._cleanupGame] Удалено сопоставление для пользователя ${idKey} с игрой ${gameId}.`); } }); this.broadcastAvailablePvPGames(); return false; } console.log(`[GameManager._cleanupGame] Очистка игры ${game.id}. Владелец: ${game.ownerIdentifier}. Причина: ${reason}. Игроков в игре: ${game.playerCount}`); if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear(); if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); if (game.gameState && !game.gameState.isGameOver) { console.log(`[GameManager._cleanupGame] Пометка игры ${game.id} как завершенной, так как она очищается во время активности.`); game.gameState.isGameOver = true; // game.io.to(game.id).emit('gameOver', { winnerId: null, reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() }); } Object.values(game.players).forEach(pInfo => { if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) { delete this.userIdentifierToGameId[pInfo.identifier]; console.log(`[GameManager._cleanupGame] Очищено userIdentifierToGameId для игрока ${pInfo.identifier}.`); } }); // Дополнительная проверка для владельца, если он не был в списке игроков (маловероятно, но для полноты) if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) { if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) { delete this.userIdentifierToGameId[game.ownerIdentifier]; console.log(`[GameManager._cleanupGame] Очищено userIdentifierToGameId для владельца ${game.ownerIdentifier} (не был в списке игроков).`); } } const pendingIdx = this.pendingPvPGames.indexOf(gameId); if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); console.log(`[GameManager._cleanupGame] ${gameId} удален из pendingPvPGames.`); } delete this.games[gameId]; console.log(`[GameManager._cleanupGame] Экземпляр игры ${gameId} удален. Осталось игр: ${Object.keys(this.games).length}. Ожидающих: ${this.pendingPvPGames.length}. Размер карты пользователей: ${Object.keys(this.userIdentifierToGameId).length}`); this.broadcastAvailablePvPGames(); return true; } getAvailablePvPGamesListForClient() { return [...this.pendingPvPGames] .map(gameId => { const game = this.games[gameId]; if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); let p1Username = 'Игрок'; let p1CharName = 'Неизвестный'; const ownerId = game.ownerIdentifier; if (p1Entry) { // Используем данные из p1Entry, если он есть (более надежно) p1Username = p1Entry.socket?.userData?.username || `User#${String(p1Entry.identifier).substring(0,4)}`; const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey); p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран'; } else if (ownerId){ // Резервный вариант, если p1Entry почему-то нет const ownerSocket = this._findClientSocketByIdentifier(ownerId); p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`; const ownerCharKey = game.playerCharacterKey; const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null; p1CharName = charData?.name || ownerCharKey || 'Не выбран'; } return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId }; } else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) { console.warn(`[GameManager.getAvailablePvPGamesListForClient] Игра ${gameId} находится в pendingPvPGames, но не является допустимой ожидающей игрой (игроков: ${game.playerCount}, завершена: ${game.gameState?.isGameOver}). Удаление.`); this._cleanupGame(gameId, 'invalid_pending_game_in_list'); } return null; }) .filter(info => info !== null); } broadcastAvailablePvPGames() { const list = this.getAvailablePvPGamesListForClient(); this.io.emit('availablePvPGamesList', list); } handleRequestGameState(socket, identifier) { const gameIdFromMap = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handleRequestGameState] Пользователь: ${identifier} (Socket: ${socket.id}) запрашивает состояние. GameID из карты: ${gameIdFromMap}`); const game = gameIdFromMap ? this.games[gameIdFromMap] : null; if (game) { const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfoInGame) { if (game.gameState?.isGameOver) { socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' }); // Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это, если игра еще в this.games return; } if (typeof game.handlePlayerReconnected === 'function') { const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket); if (!reconnected) { console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected для ${identifier} в ${game.id} вернул false.`); // GameInstance должен был отправить ошибку. } } else { console.error(`[GameManager.handleRequestGameState] КРИТИЧЕСКИ: GameInstance ${game.id} отсутствует handlePlayerReconnected!`); this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request'); } } else { // Игрок сопоставлен с игрой, но НЕ НАЙДЕН в game.players. Это может произойти, если PCH еще не добавил игрока (например, F5 на экране создания игры). // Попытаемся добавить игрока в игру, если это PvP и есть место, или если это его же игра в режиме AI. console.warn(`[GameManager.handleRequestGameState] Пользователь ${identifier} сопоставлен с игрой ${gameIdFromMap}, но НЕ НАЙДЕН в game.players. Попытка добавить/переподключить.`); if (game.mode === 'pvp') { // Пытаемся присоединить, предполагая, что он мог быть удален или это F5 перед полным присоединением const chosenCharKey = socket.handshake.query.chosenCharacterKey || 'elena'; // Получаем ключ из запроса или дефолтный if (game.addPlayer(socket, chosenCharKey, identifier)) { // Успешно добавили или переподключили через addPlayer -> handlePlayerReconnected const newPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier); socket.emit('gameCreated', { // Отправляем событие, как при обычном присоединении gameId: game.id, mode: game.mode, yourPlayerId: newPlayerInfo.id, chosenCharacterKey: newPlayerInfo.chosenCharacterKey }); if (game.playerCount === 2) { // Если игра стала полной if(game.initializeGame()) game.startGame(); else this._cleanupGame(game.id, 'init_fail_pvp_readd_gm'); const idx = this.pendingPvPGames.indexOf(game.id); if (idx > -1) this.pendingPvPGames.splice(idx, 1); this.broadcastAvailablePvPGames(); } } else { // Не удалось добавить/переподключить через addPlayer this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_readd_failed_in_gi_on_request'); } } else if (game.mode === 'ai' && game.ownerIdentifier === identifier) { // Для AI игры, если это владелец, пытаемся через handlePlayerReconnected if (typeof game.handlePlayerReconnected === 'function') { // Предполагаем, что роль PLAYER_ID, так как это AI игра и он владелец const reconnected = game.handlePlayerReconnected(GAME_CONFIG.PLAYER_ID, socket); if (!reconnected) { this._handleGameRecoveryError(socket, game.id, identifier, 'ai_owner_reconnect_failed_on_request'); } } else { this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_ai_owner_on_request'); } } else { this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_unhandled_case_on_request'); } } } else { socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' }); if (this.userIdentifierToGameId[identifier]) { console.warn(`[GameManager.handleRequestGameState] Экземпляр игры для gameId ${gameIdFromMap} (пользователь ${identifier}) не найден. Очистка устаревшей записи в карте.`); delete this.userIdentifierToGameId[identifier]; } } } _handleGameRecoveryError(socket, gameId, identifier, reasonCode) { console.error(`[GameManager._handleGameRecoveryError] Ошибка восстановления игры (ID: ${gameId || 'N/A'}) для пользователя ${identifier}. Причина: ${reasonCode}.`); socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' }); if (gameId && this.games[gameId]) { this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`); } else if (this.userIdentifierToGameId[identifier]) { const problematicGameIdForUser = this.userIdentifierToGameId[identifier]; delete this.userIdentifierToGameId[identifier]; console.log(`[GameManager._handleGameRecoveryError] Очищено устаревшее userIdentifierToGameId[${identifier}], указывающее на ${problematicGameIdForUser}.`); } if (this.userIdentifierToGameId[identifier]) { // Финальная проверка delete this.userIdentifierToGameId[identifier]; console.warn(`[GameManager._handleGameRecoveryError] Принудительно очищено userIdentifierToGameId[${identifier}] в качестве финальной меры.`); } socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' }); } } module.exports = GameManager;