// /server/game/instance/PlayerConnectionHandler.js const GAME_CONFIG = require('../../core/config'); const dataUtils = require('../../data/dataUtils'); class PlayerConnectionHandler { constructor(gameInstance) { this.gameInstance = gameInstance; // Ссылка на основной GameInstance this.io = gameInstance.io; this.gameId = gameInstance.id; this.mode = gameInstance.mode; this.players = {}; // { socket.id: { id, socket, chosenCharacterKey, identifier, isTemporarilyDisconnected, name (optional from gameState) } } this.playerSockets = {}; // { playerIdRole: socket } // Авторитетный сокет для роли this.playerCount = 0; this.reconnectTimers = {}; // { playerIdRole: { timerId, updateIntervalId, startTimeMs, durationMs } } this.pausedTurnState = null; // { remainingTime, forPlayerRoleIsPlayer, isAiCurrentlyMoving } console.log(`[PCH for Game ${this.gameId}] Инициализирован.`); } addPlayer(socket, chosenCharacterKey = 'elena', identifier) { console.log(`[PCH ${this.gameId}] Попытка addPlayer. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`); const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier); if (existingPlayerByIdentifier) { console.warn(`[PCH ${this.gameId}] Идентификатор ${identifier} уже связан с ролью игрока ${existingPlayerByIdentifier.id} (сокет ${existingPlayerByIdentifier.socket?.id}). Обрабатывается как возможное переподключение.`); if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { console.warn(`[PCH ${this.gameId}] Игрок ${identifier} пытается (пере)присоединиться к уже завершенной игре. Отправка gameError.`); socket.emit('gameError', { message: 'Эта игра уже завершена.' }); return false; } // Если игрок уже есть, и это не временное отключение, и сокет другой - это F5 или новая вкладка. // GameManager должен был направить на handleRequestGameState, который вызовет handlePlayerReconnected. // Прямой addPlayer в этом случае - редкий сценарий, но handlePlayerReconnected его обработает. return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket); } if (Object.keys(this.players).length >= 2 && this.playerCount >=2 && this.mode === 'pvp') { // В AI режиме только 1 человек socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); return false; } if (this.mode === 'ai' && this.playerCount >=1) { socket.emit('gameError', { message: 'К AI игре может присоединиться только один игрок.'}); return false; } let assignedPlayerId; let actualCharacterKey = chosenCharacterKey || 'elena'; const charData = dataUtils.getCharacterData(actualCharacterKey); if (this.mode === 'ai') { // if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { // Эта проверка уже покрыта playerCount >= 1 выше // socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); // return false; // } assignedPlayerId = GAME_CONFIG.PLAYER_ID; } else { // pvp if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) { assignedPlayerId = GAME_CONFIG.PLAYER_ID; } else if (!this.playerSockets[GAME_CONFIG.OPPONENT_ID]) { assignedPlayerId = GAME_CONFIG.OPPONENT_ID; const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) { if (actualCharacterKey === 'elena') actualCharacterKey = 'almagest'; else if (actualCharacterKey === 'almagest') actualCharacterKey = 'elena'; else actualCharacterKey = dataUtils.getAllCharacterKeys().find(k => k !== firstPlayerInfo.chosenCharacterKey) || 'elena'; } } else { // Оба слота заняты, но playerCount мог быть < 2 если кто-то в процессе дисконнекта socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре (возможно, все заняты или в процессе переподключения).' }); return false; } } // Если для этой роли УЖЕ был игрок (например, старый сокет при F5 до того, как сработал disconnect), // то handlePlayerReconnected должен был бы это обработать. Этот блок здесь - подстраховка, // если addPlayer вызван напрямую в таком редком случае. const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId && this.players[sid].socket?.id !== socket.id); if (oldPlayerSocketIdForRole) { const oldPlayerInfo = this.players[oldPlayerSocketIdForRole]; console.warn(`[PCH ${this.gameId}] addPlayer: Найден старый сокет ${oldPlayerInfo.socket?.id} для роли ${assignedPlayerId}. Удаляем его запись.`); if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.gameId); oldPlayerInfo.socket.disconnect(true); } catch(e){} } delete this.players[oldPlayerSocketIdForRole]; } this.players[socket.id] = { id: assignedPlayerId, socket: socket, chosenCharacterKey: actualCharacterKey, identifier: identifier, isTemporarilyDisconnected: false, name: charData?.baseStats?.name || actualCharacterKey }; this.playerSockets[assignedPlayerId] = socket; this.playerCount++; socket.join(this.gameId); console.log(`[PCH ${this.gameId}] Сокет ${socket.id} присоединен к комнате ${this.gameId} (addPlayer).`); if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey); else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.gameInstance.setOpponentCharacterKey(actualCharacterKey); if (!this.gameInstance.ownerIdentifier && (this.mode === 'ai' || (this.mode === 'pvp' && assignedPlayerId === GAME_CONFIG.PLAYER_ID))) { this.gameInstance.setOwnerIdentifier(identifier); } console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Socket: ${socket.id}) добавлен как ${assignedPlayerId} с персонажем ${this.players[socket.id].name}. Активных игроков: ${this.playerCount}. Владелец: ${this.gameInstance.ownerIdentifier}`); return true; } removePlayer(socketId, reason = "unknown_reason_for_removal") { const playerInfo = this.players[socketId]; if (playerInfo) { const playerRole = playerInfo.id; const playerIdentifier = playerInfo.identifier; console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`); if (playerInfo.socket) { try { playerInfo.socket.leave(this.gameId); } catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave: ${e.message}`); } } if (!playerInfo.isTemporarilyDisconnected) { this.playerCount--; } delete this.players[socketId]; if (this.playerSockets[playerRole]?.id === socketId) { delete this.playerSockets[playerRole]; } this.clearReconnectTimer(playerRole); console.log(`[PCH ${this.gameId}] Игрок ${playerIdentifier} удален. Активных игроков сейчас: ${this.playerCount}.`); this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason); } else { console.warn(`[PCH ${this.gameId}] removePlayer вызван для неизвестного socketId: ${socketId}`); } } handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) { console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft для роли ${playerIdRole}, id ${identifier}, char ${characterKey}, disconnectedSocketId ${disconnectedSocketId}`); const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); if (!playerEntry || !playerEntry.socket) { console.warn(`[PCH ${this.gameId}] Запись игрока или сокет не найдены для ${identifier} (роль ${playerIdRole}) во время потенциального выхода. disconnectedSocketId: ${disconnectedSocketId}`); // Если записи нет, возможно, игрок уже удален или это был очень старый сокет. // Проверим, есть ли запись по disconnectedSocketId, и если да, удалим ее. if (this.players[disconnectedSocketId]) { console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId}, удаляем ее.`); this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_entry'); } return; } if (playerEntry.socket.id !== disconnectedSocketId) { console.log(`[PCH ${this.gameId}] Событие отключения для УСТАРЕВШЕГО сокета ${disconnectedSocketId} для игрока ${identifier} (Роль ${playerIdRole}). Текущий активный сокет: ${playerEntry.socket.id}. Игрок, вероятно, уже переподключился или сессия обновлена. Игнорируем дальнейшую логику "потенциального выхода" для этого устаревшего сокета.`); if (this.players[disconnectedSocketId]) { delete this.players[disconnectedSocketId]; // Удаляем только эту запись, не вызываем полный removePlayer } return; } if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { console.log(`[PCH ${this.gameId}] Игра уже завершена, не обрабатываем потенциальный выход для ${identifier}.`); return; } if (playerEntry.isTemporarilyDisconnected) { console.log(`[PCH ${this.gameId}] Игрок ${identifier} уже помечен как временно отключенный.`); return; } playerEntry.isTemporarilyDisconnected = true; this.playerCount--; console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`); const disconnectedName = playerEntry.name || this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`; this.gameInstance.addToLog(`🔌 Игрок ${disconnectedName} отключился. Ожидание переподключения...`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.gameInstance.broadcastLogUpdate(); const otherPlayerRole = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const otherSocket = this.playerSockets[otherPlayerRole]; const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole); if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { otherSocket.emit('opponentDisconnected', { disconnectedPlayerId: playerIdRole, disconnectedCharacterName: disconnectedName, }); } if (this.gameInstance.turnTimer && (this.gameInstance.turnTimer.isActive() || (this.mode === 'ai' && this.gameInstance.turnTimer.isConfiguredForAiMove))) { this.pausedTurnState = this.gameInstance.turnTimer.pause(); console.log(`[PCH ${this.gameId}] Таймер хода приостановлен из-за отключения. Состояние:`, JSON.stringify(this.pausedTurnState)); } else { this.pausedTurnState = null; } this.clearReconnectTimer(playerIdRole); const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000; const reconnectStartTime = Date.now(); const updateInterval = setInterval(() => { const remaining = reconnectDuration - (Date.now() - reconnectStartTime); if (remaining <= 0 || !this.reconnectTimers[playerIdRole] || this.reconnectTimers[playerIdRole]?.timerId === null) { // Добавлена проверка на существование таймера if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); if (this.reconnectTimers[playerIdRole]) this.reconnectTimers[playerIdRole].updateIntervalId = null; // Помечаем, что интервал очищен this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 }); return; } this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: Math.ceil(remaining) }); }, 1000); const timeoutId = setTimeout(() => { if (this.reconnectTimers[playerIdRole]?.updateIntervalId) { // Очищаем интервал, если он еще существует clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); this.reconnectTimers[playerIdRole].updateIntervalId = null; } this.reconnectTimers[playerIdRole].timerId = null; // Помечаем, что основной таймаут сработал или очищен const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) { this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout"); } }, reconnectDuration); this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration }; } handlePlayerReconnected(playerIdRole, newSocket) { const identifier = newSocket.userData?.userId; console.log(`[PCH RECONNECT_ATTEMPT] gameId: ${this.gameId}, Role: ${playerIdRole}, Identifier: ${identifier}, NewSocket: ${newSocket.id}`); if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { newSocket.emit('gameError', { message: 'Игра уже завершена.' }); return false; } let playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); console.log(`[PCH RECONNECT_ATTEMPT] Found playerEntry:`, playerEntry ? {id: playerEntry.id, identifier: playerEntry.identifier, oldSocketId: playerEntry.socket?.id, isTempDisc: playerEntry.isTemporarilyDisconnected} : null); if (playerEntry) { const oldSocket = playerEntry.socket; // Обновляем сокет в playerEntry и в this.players / this.playerSockets, если сокет новый if (oldSocket && oldSocket.id !== newSocket.id) { console.log(`[PCH ${this.gameId}] New socket ${newSocket.id} for player ${identifier}. Old socket: ${oldSocket.id}. Updating records.`); if (this.players[oldSocket.id]) delete this.players[oldSocket.id]; // Удаляем старую запись по старому socket.id if (oldSocket.connected) { // Пытаемся корректно закрыть старый сокет console.log(`[PCH ${this.gameId}] Disconnecting old stale socket ${oldSocket.id}.`); oldSocket.disconnect(true); } } playerEntry.socket = newSocket; // Обновляем сокет в существующей playerEntry this.players[newSocket.id] = playerEntry; // Убеждаемся, что по новому ID есть актуальная запись if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) { // Если вдруг playerEntry был взят по старому socket.id, и этот ID теперь должен быть удален delete this.players[oldSocket.id]; } this.playerSockets[playerIdRole] = newSocket; // Обновляем авторитетный сокет для роли // Всегда заново присоединяем сокет к комнате console.log(`[PCH ${this.gameId}] Forcing newSocket ${newSocket.id} (identifier: ${identifier}) to join room ${this.gameId} during reconnect.`); newSocket.join(this.gameId); if (playerEntry.isTemporarilyDisconnected) { console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`); this.clearReconnectTimer(playerIdRole); // Очищаем таймер реконнекта this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); // Сообщаем UI, что таймер остановлен playerEntry.isTemporarilyDisconnected = false; this.playerCount++; // Восстанавливаем счетчик активных игроков } else { // Игрок не был помечен как временно отключенный. // Это может быть F5 или запрос состояния на "том же" (или новом, но старый не отвалился) сокете. // playerCount не меняется, т.к. игрок считался активным. console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Old socket ID: ${oldSocket?.id}`); } // Обновление имени if (this.gameInstance.gameState && this.gameInstance.gameState[playerIdRole]?.name) { playerEntry.name = this.gameInstance.gameState[playerIdRole].name; } else { const charData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); playerEntry.name = charData?.baseStats?.name || playerEntry.chosenCharacterKey; } console.log(`[PCH ${this.gameId}] Имя игрока ${identifier} обновлено/установлено на: ${playerEntry.name}`); this.gameInstance.addToLog(`🔌 Игрок ${playerEntry.name || identifier} снова в игре! (Сессия обновлена)`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.sendFullGameStateOnReconnect(newSocket, playerEntry, playerIdRole); if (playerEntry.isTemporarilyDisconnected === false && this.pausedTurnState) { // Если игрок был временно отключен, isTemporarilyDisconnected уже false this.resumeGameLogicAfterReconnect(playerIdRole); } else if (playerEntry.isTemporarilyDisconnected === false && !this.pausedTurnState) { // Игрок не был temp disconnected, и не было сохраненного состояния таймера (значит, он и не останавливался из-за этого игрока) // Просто отправляем текущее состояние таймера, если он активен console.log(`[PCH ${this.gameId}] Player was not temp disconnected, and no pausedTurnState. Forcing timer update if active.`); if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive() && this.gameInstance.turnTimer.onTickCallback) { const tt = this.gameInstance.turnTimer; const elapsedTime = Date.now() - tt.segmentStartTimeMs; const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime); tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState); } else if (this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused() && !this.isGameEffectivelyPaused()) { // Если таймер не активен, не на паузе, и игра не на общей паузе - возможно, его нужно запустить (если сейчас ход этого игрока) const gs = this.gameInstance.gameState; if (gs && !gs.isGameOver) { const isHisTurnNow = (gs.isPlayerTurn && playerIdRole === GAME_CONFIG.PLAYER_ID) || (!gs.isPlayerTurn && playerIdRole === GAME_CONFIG.OPPONENT_ID); const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn; if(isHisTurnNow || isAiTurnNow) { console.log(`[PCH ${this.gameId}] Timer not active, not paused. Game not paused. Attempting to start timer for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`); this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow); if (isAiTurnNow && !this.gameInstance.turnTimer.isConfiguredForAiMove && !this.gameInstance.turnTimer.isCurrentlyRunning) { // Доп. проверка, чтобы AI точно пошел, если это его ход и таймер не стартовал для него как "AI move" setTimeout(() => { if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) { this.gameInstance.processAiTurn(); } }, GAME_CONFIG.DELAY_OPPONENT_TURN); } } } } } return true; } else { // playerEntry не найден console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена. Это может быть новый игрок или сессия истекла.`); // Если это новый игрок для этой роли, то addPlayer должен был быть вызван GameManager'ом. // Если PCH вызывается напрямую, и игрока нет, это ошибка или устаревший запрос. newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена). Попробуйте создать игру заново.' }); return false; } } sendFullGameStateOnReconnect(socket, playerEntry, playerIdRole) { console.log(`[PCH SEND_STATE_RECONNECT] gameId: ${this.gameId}, Role: ${playerIdRole}, Identifier: ${playerEntry.identifier}`); if (!this.gameInstance.gameState) { console.log(`[PCH SEND_STATE_RECONNECT] gameState отсутствует, попытка инициализации...`); if (!this.gameInstance.initializeGame()) { // initializeGame должен установить gameState this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch_helper', 'PCH Helper: GS null после повторной инициализации при переподключении.'); return; } console.log(`[PCH SEND_STATE_RECONNECT] gameState инициализирован. Player: ${this.gameInstance.gameState.player.name}, Opponent: ${this.gameInstance.gameState.opponent.name}`); } const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; // Получаем ключ оппонента из gameState ИЛИ из сохраненных ключей в GameInstance let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey); const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; // Обновляем имена в gameState на основе сохраненных в PCH или данных персонажей if (this.gameInstance.gameState) { if (this.gameInstance.gameState[playerIdRole]) { this.gameInstance.gameState[playerIdRole].name = playerEntry.name || pData?.baseStats?.name || 'Игрок'; } const opponentPCHEntry = Object.values(this.players).find(p => p.id === oppRoleKey); if (this.gameInstance.gameState[oppRoleKey]) { if (opponentPCHEntry?.name) { this.gameInstance.gameState[oppRoleKey].name = opponentPCHEntry.name; } else if (oData?.baseStats?.name) { this.gameInstance.gameState[oppRoleKey].name = oData.baseStats.name; } else if (this.mode === 'ai' && oppRoleKey === GAME_CONFIG.OPPONENT_ID) { this.gameInstance.gameState[oppRoleKey].name = 'Балард'; // Фоллбэк для AI } else { this.gameInstance.gameState[oppRoleKey].name = 'Оппонент'; } } } console.log(`[PCH SEND_STATE_RECONNECT] Отправка gameStarted. Player GS: ${this.gameInstance.gameState?.player?.name}, Opponent GS: ${this.gameInstance.gameState?.opponent?.name}. IsPlayerTurn: ${this.gameInstance.gameState?.isPlayerTurn}`); socket.emit('gameStarted', { // Используем 'gameStarted' для полной синхронизации состояния gameId: this.gameId, yourPlayerId: playerIdRole, initialGameState: this.gameInstance.gameState, playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats || {name: (this.mode === 'pvp' ? 'Ожидание...' : 'Противник AI'), maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null}, playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities || [], log: this.gameInstance.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } }); } resumeGameLogicAfterReconnect(reconnectedPlayerIdRole) { const playerEntry = Object.values(this.players).find(p => p.id === reconnectedPlayerIdRole); const reconnectedName = playerEntry?.name || this.gameInstance.gameState?.[reconnectedPlayerIdRole]?.name || `Игрок (Роль ${reconnectedPlayerIdRole})`; console.log(`[PCH RESUME_LOGIC] gameId: ${this.gameId}, Role: ${reconnectedPlayerIdRole}, Name: ${reconnectedName}, PausedState: ${JSON.stringify(this.pausedTurnState)}, TimerActive: ${this.gameInstance.turnTimer?.isActive()}, GS.isPlayerTurn: ${this.gameInstance.gameState?.isPlayerTurn}`); const otherPlayerRole = reconnectedPlayerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const otherSocket = this.playerSockets[otherPlayerRole]; const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole); if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { otherSocket.emit('playerReconnected', { reconnectedPlayerId: reconnectedPlayerIdRole, reconnectedPlayerName: reconnectedName }); if (this.gameInstance.logBuffer.length > 0) { // Отправляем накопившиеся логи другому игроку otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() }); } } // Обновляем состояние для всех (включая переподключившегося, т.к. его лог мог быть уже потреблен) this.gameInstance.broadcastGameStateUpdate(); // Это отправит gameState и оставшиеся логи if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) { // this.gameInstance.broadcastGameStateUpdate(); // Перенесено выше if (Object.keys(this.reconnectTimers).length === 0) { // Только если нет других ожидающих реконнекта const currentTurnIsForPlayerInGS = this.gameInstance.gameState.isPlayerTurn; const isCurrentTurnAiForTimer = this.mode === 'ai' && !currentTurnIsForPlayerInGS; let resumedFromPausedState = false; if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') { const gsTurnMatchesPausedTurn = (currentTurnIsForPlayerInGS && this.pausedTurnState.forPlayerRoleIsPlayer) || (!currentTurnIsForPlayerInGS && !this.pausedTurnState.forPlayerRoleIsPlayer); if (gsTurnMatchesPausedTurn) { console.log(`[PCH ${this.gameId}] Возобновляем таймер хода из pausedTurnState. Время: ${this.pausedTurnState.remainingTime}мс. Для игрока (в pausedState): ${this.pausedTurnState.forPlayerRoleIsPlayer}. GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход (в pausedState): ${this.pausedTurnState.isAiCurrentlyMoving}`); this.gameInstance.turnTimer.resume( this.pausedTurnState.remainingTime, this.pausedTurnState.forPlayerRoleIsPlayer, // Это isConfiguredForPlayerSlotTurn для таймера this.pausedTurnState.isAiCurrentlyMoving // Это isConfiguredForAiMove для таймера ); resumedFromPausedState = true; } else { console.warn(`[PCH ${this.gameId}] pausedTurnState (${JSON.stringify(this.pausedTurnState)}) не совпадает с текущим ходом в gameState (isPlayerTurn: ${currentTurnIsForPlayerInGS}). Сбрасываем pausedTurnState и запускаем таймер заново, если нужно.`); } this.pausedTurnState = null; // Сбрасываем в любом случае } if (!resumedFromPausedState && this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused()) { console.log(`[PCH ${this.gameId}] Запускаем таймер хода заново после реконнекта (pausedState не использовался или был неактуален, таймер неактивен и не на паузе). GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход для таймера: ${isCurrentTurnAiForTimer}`); this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isCurrentTurnAiForTimer); if (isCurrentTurnAiForTimer && !this.gameInstance.turnTimer.isConfiguredForAiMove && !this.gameInstance.turnTimer.isCurrentlyRunning) { setTimeout(() => { if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) { this.gameInstance.processAiTurn(); } }, GAME_CONFIG.DELAY_OPPONENT_TURN); } } else if (!resumedFromPausedState && this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive()){ console.log(`[PCH ${this.gameId}] Таймер уже был активен при попытке перезапуска после реконнекта (pausedTurnState не использовался/неактуален). Ничего не делаем с таймером.`); } } else { console.log(`[PCH ${this.gameId}] Возобновление логики таймера отложено, есть другие активные таймеры реконнекта: ${Object.keys(this.reconnectTimers)}`); } } else { console.log(`[PCH ${this.gameId}] Игра на паузе или завершена, логика таймера не возобновляется. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameInstance.gameState?.isGameOver}`); } } clearReconnectTimer(playerIdRole) { if (this.reconnectTimers[playerIdRole]) { clearTimeout(this.reconnectTimers[playerIdRole].timerId); this.reconnectTimers[playerIdRole].timerId = null; // Явно обнуляем if (this.reconnectTimers[playerIdRole].updateIntervalId) { clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); this.reconnectTimers[playerIdRole].updateIntervalId = null; // Явно обнуляем } delete this.reconnectTimers[playerIdRole]; // Удаляем всю запись console.log(`[PCH ${this.gameId}] Очищен таймер переподключения для роли ${playerIdRole}.`); } } clearAllReconnectTimers() { console.log(`[PCH ${this.gameId}] Очистка ВСЕХ таймеров переподключения.`); for (const roleId in this.reconnectTimers) { this.clearReconnectTimer(roleId); } } isGameEffectivelyPaused() { if (this.mode === 'pvp') { if (this.playerCount < 2 && Object.keys(this.players).length > 0) { const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID); if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) { return true; } } } else if (this.mode === 'ai') { const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); return humanPlayer?.isTemporarilyDisconnected ?? false; // Если игрока нет, не на паузе. Если есть - зависит от его состояния. } return false; } getAllPlayersInfo() { return { ...this.players }; } getPlayerSockets() { return { ...this.playerSockets }; } } module.exports = PlayerConnectionHandler;