From daccc6068907f6299ed875412a801199dd1a5d92 Mon Sep 17 00:00:00 2001 From: PsiMagistr Date: Tue, 27 May 2025 11:00:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20gameInstance.=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20PlayerConnectionHandler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/game/GameManager.js | 73 +- server/game/instance/GameInstance.js | 646 +++++++----------- .../game/instance/PlayerConnectionHandler.js | 397 +++++++++++ 3 files changed, 673 insertions(+), 443 deletions(-) create mode 100644 server/game/instance/PlayerConnectionHandler.js diff --git a/server/game/GameManager.js b/server/game/GameManager.js index e066e3d..4dfd29d 100644 --- a/server/game/GameManager.js +++ b/server/game/GameManager.js @@ -1,6 +1,6 @@ // /server/game/GameManager.js const { v4: uuidv4 } = require('uuid'); -const GameInstance = require('./instance/GameInstance'); +const GameInstance = require('./instance/GameInstance'); // Путь к GameInstance с геттерами const dataUtils = require('../data/dataUtils'); const GAME_CONFIG = require('../core/config'); @@ -19,9 +19,9 @@ class GameManager { if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) { const gameToRemove = this.games[oldPendingGameId]; - // Используем game.playerCount (или аналогичный метод GameInstance, если он инкапсулирует это) + // Используем gameToRemove.playerCount (через геттер) if (gameToRemove.mode === 'pvp' && - gameToRemove.playerCount === 1 && // Предполагаем, GameInstance.playerCount - это активные игроки + gameToRemove.playerCount === 1 && gameToRemove.ownerIdentifier === identifier && this.pendingPvPGames.includes(oldPendingGameId)) { console.log(`[GameManager._removePreviousPendingGames] User ${identifier} creating/joining new. Removing previous pending PvP game: ${oldPendingGameId}`); @@ -36,10 +36,11 @@ class GameManager { const existingGameId = this.userIdentifierToGameId[identifier]; if (existingGameId && this.games[existingGameId]) { const existingGame = this.games[existingGameId]; - // Используем game.playerCount + // Используем existingGame.playerCount (через геттер) console.warn(`[GameManager.createGame] User ${identifier} already in game ${existingGameId}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`); if (existingGame.gameState && !existingGame.gameState.isGameOver) { + // Используем existingGame.playerCount (через геттер) if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) { socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' }); } else { @@ -60,10 +61,9 @@ class GameManager { const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena'); - // addPlayer в GameInstance теперь bool, а не объект с результатом if (game.addPlayer(socket, charKeyForPlayer, identifier)) { this.userIdentifierToGameId[identifier] = gameId; - // Получаем роль и актуальный ключ из GameInstance после добавления + // Получаем роль и актуальный ключ из GameInstance через геттер game.players const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); const assignedPlayerId = playerInfo?.id; const actualCharacterKey = playerInfo?.chosenCharacterKey; @@ -90,7 +90,7 @@ class GameManager { this._cleanupGame(gameId, 'init_fail_ai_create_gm'); } } else if (mode === 'pvp') { - game.initializeGame(); // Инициализирует первого игрока + game.initializeGame(); if (!this.pendingPvPGames.includes(gameId)) { this.pendingPvPGames.push(gameId); } @@ -111,7 +111,9 @@ class GameManager { if (game.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_${identifier}`); return; } if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; } + // Используем геттер game.players const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); + // Используем game.playerCount (через геттер) if (game.playerCount >= 2 && !playerInfoInGame?.isTemporarilyDisconnected) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } @@ -130,12 +132,11 @@ class GameManager { const charKeyForJoin = chosenCharacterKey || 'elena'; if (game.addPlayer(socket, charKeyForJoin, identifier)) { this.userIdentifierToGameId[identifier] = gameIdToJoin; - const joinedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier); // Получаем инфо после добавления + // Используем геттер game.players + const joinedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier); if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) { - console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}. Cleaning up.`); - // Не вызываем _cleanupGame здесь, т.к. игра могла быть уже с одним игроком. - // GameInstance.addPlayer должен был бы вернуть false и не изменить playerCount. + console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`); socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' }); return; } @@ -147,7 +148,8 @@ class GameManager { chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey }); - if (game.playerCount === 2) { // Используем game.playerCount из GameInstance + // Используем game.playerCount (через геттер) + if (game.playerCount === 2) { console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`); if (game.initializeGame()) { game.startGame(); @@ -162,7 +164,6 @@ class GameManager { } findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { - // ... (Логика findAndJoinRandomPvPGame без изменений, использует game.playerCount) console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`); const existingGameId = this.userIdentifierToGameId[identifier]; if (existingGameId && this.games[existingGameId]) { @@ -179,6 +180,7 @@ class GameManager { let gameIdToJoin = null; for (const id of [...this.pendingPvPGames]) { const pendingGame = this.games[id]; + // Используем pendingGame.playerCount (через геттер) if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier && @@ -205,11 +207,12 @@ class GameManager { const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { - const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket; // Находим сокет по identifier + // Используем геттер game.players + const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket; if (playerSocket) this.handleRequestGameState(playerSocket, identifier); return; } - game.processPlayerAction(identifier, actionData); // Передаем identifier + game.processPlayerAction(identifier, actionData); } else { delete this.userIdentifierToGameId[identifier]; const clientSocket = this._findClientSocketByIdentifier(identifier); @@ -218,7 +221,6 @@ class GameManager { } handlePlayerSurrender(identifier) { - // ... (Логика handlePlayerSurrender без изменений) const gameId = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`); const game = this.games[gameId]; @@ -260,7 +262,6 @@ class GameManager { } _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) return s; @@ -278,13 +279,11 @@ class GameManager { console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER.`); return; } - - // Находим информацию об игроке в инстансе игры по identifier + // Используем геттер game.players const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) { console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active player ${identifier} (Role: ${playerInfoInGame.id}) in game ${gameIdFromMap}. Notifying GameInstance.`); - // Передаем роль, идентификатор и ключ персонажа в GameInstance if (typeof game.handlePlayerPotentiallyLeft === 'function') { game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey); } else { @@ -296,7 +295,7 @@ class GameManager { } else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) { console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already temp disconnected. Reconnect timer handles final cleanup.`); } else if (!playerInfoInGame) { - console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. Clearing map.`); + console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game's player list. Clearing map.`); if (this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier]; } } else { @@ -307,7 +306,6 @@ class GameManager { } _cleanupGame(gameId, reason = 'unknown') { - // ... (Код _cleanupGame почти без изменений, но использует game.playerCount и game.ownerIdentifier) console.log(`[GameManager._cleanupGame] Attempting cleanup for GameID: ${gameId}, Reason: ${reason}`); const game = this.games[gameId]; @@ -317,24 +315,26 @@ class GameManager { for (const idKey in this.userIdentifierToGameId) { if (this.userIdentifierToGameId[idKey] === gameId) delete this.userIdentifierToGameId[idKey]; } return false; } - + // Используем game.playerCount (через геттер) console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`); if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear(); - if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); // Вызываем у GameInstance + if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); if (game.gameState && !game.gameState.isGameOver) { game.gameState.isGameOver = true; } let playersCleanedFromMap = 0; - Object.values(game.players).forEach(pInfo => { // Игроки теперь в game.players + // Используем геттер game.players + Object.values(game.players).forEach(pInfo => { if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) { delete this.userIdentifierToGameId[pInfo.identifier]; playersCleanedFromMap++; } }); + // Используем геттер game.players if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId && - !Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Проверка, если владелец не в списке игроков + !Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { delete this.userIdentifierToGameId[game.ownerIdentifier]; playersCleanedFromMap++; } @@ -349,12 +349,12 @@ class GameManager { } getAvailablePvPGamesListForClient() { - // ... (Код без изменений, использует game.playerCount и game.ownerIdentifier) return this.pendingPvPGames .map(gameId => { const game = this.games[gameId]; + // Используем game.playerCount (через геттер) if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { - // Находим первого игрока (владельца) в инстансе игры + // Используем геттер game.players const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); let p1Username = 'Игрок'; let p1CharName = 'Неизвестный'; @@ -364,10 +364,10 @@ class GameManager { 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){ // Фоллбэк на поиск по ownerId, если p1Entry не найден или без userData + } else if (ownerId){ const ownerSocket = this._findClientSocketByIdentifier(ownerId); p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`; - const ownerCharKey = game.playerCharacterKey; // Ключ персонажа первого игрока из GameInstance + const ownerCharKey = game.playerCharacterKey; const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null; p1CharName = charData?.name || ownerCharKey || 'Не выбран'; } @@ -389,7 +389,8 @@ class GameManager { const game = gameIdFromMap ? this.games[gameIdFromMap] : null; if (game) { - const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); // Ищем по identifier + // Используем геттер game.players + const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`); if (playerInfoInGame) { @@ -398,7 +399,6 @@ class GameManager { if(this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier]; return; } - // Передаем РОЛЬ и НОВЫЙ СОКЕТ в GameInstance для обработки реконнекта if (typeof game.handlePlayerReconnected === 'function') { const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket); // ... (обработка результата reconnected, если нужно) @@ -416,21 +416,18 @@ class GameManager { } _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 && this.games[gameId]) { // Проверяем, что игра еще существует перед очисткой + if (gameId && this.games[gameId]) { this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`); } else if (this.userIdentifierToGameId[identifier]) { - // Если игра уже удалена, но пользователь все еще к ней привязан const problematicGameId = this.userIdentifierToGameId[identifier]; - if (this.games[problematicGameId]) { // Если она все же есть + if (this.games[problematicGameId]) { this._cleanupGame(problematicGameId, `recovery_error_stale_map_${identifier}_reason_${reasonCode}`); - } else { // Если ее нет, просто чистим карту + } else { delete this.userIdentifierToGameId[identifier]; } } - // Если после _cleanupGame пользователь все еще привязан (маловероятно, но для гарантии) if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier]; socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' }); } diff --git a/server/game/instance/GameInstance.js b/server/game/instance/GameInstance.js index c479228..9b5a239 100644 --- a/server/game/instance/GameInstance.js +++ b/server/game/instance/GameInstance.js @@ -4,6 +4,7 @@ const TurnTimer = require('./TurnTimer'); const gameLogic = require('../logic'); const dataUtils = require('../../data/dataUtils'); const GAME_CONFIG = require('../../core/config'); +const PlayerConnectionHandler = require('./PlayerConnectionHandler'); class GameInstance { constructor(gameId, io, mode = 'ai', gameManager) { @@ -12,9 +13,7 @@ class GameInstance { this.mode = mode; this.gameManager = gameManager; - this.players = {}; - this.playerSockets = {}; - this.playerCount = 0; + this.playerConnectionHandler = new PlayerConnectionHandler(this); this.gameState = null; this.aiOpponent = (mode === 'ai'); @@ -24,9 +23,6 @@ class GameInstance { this.opponentCharacterKey = null; this.ownerIdentifier = null; - this.reconnectTimers = {}; - this.pausedTurnState = null; - this.turnTimer = new TurnTimer( GAME_CONFIG.TURN_DURATION_MS, GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS, @@ -35,7 +31,7 @@ class GameInstance { this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: isPlayerTurnForTimer, - isPaused: isPaused + isPaused: isPaused || this.isGameEffectivelyPaused() }); }, this.id @@ -44,23 +40,73 @@ class GameInstance { if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') { console.error(`[GameInstance ${this.id}] CRITICAL ERROR: GameManager reference invalid.`); } - console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}.`); + console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}. PlayerConnectionHandler also initialized.`); + } + + // --- Геттеры для GameManager и внутреннего использования --- + get playerCount() { + return this.playerConnectionHandler.playerCount; + } + + // Этот геттер может быть полезен, если GameManager или другая часть GameInstance + // захочет получить доступ ко всем данным игроков, не зная о PCH. + get players() { + return this.playerConnectionHandler.getAllPlayersInfo(); + } + + // --- Сеттеры для PCH --- + setPlayerCharacterKey(key) { this.playerCharacterKey = key; } + setOpponentCharacterKey(key) { this.opponentCharacterKey = key; } + setOwnerIdentifier(identifier) { this.ownerIdentifier = identifier; } + + // --- Методы, делегирующие PCH --- + addPlayer(socket, chosenCharacterKey, identifier) { + return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier); + } + + removePlayer(socketId, reason) { + this.playerConnectionHandler.removePlayer(socketId, reason); + } + + handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { + this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey); + } + + handlePlayerReconnected(playerIdRole, newSocket) { + return this.playerConnectionHandler.handlePlayerReconnected(playerIdRole, newSocket); + } + + clearAllReconnectTimers() { + this.playerConnectionHandler.clearAllReconnectTimers(); + } + + isGameEffectivelyPaused() { + return this.playerConnectionHandler.isGameEffectivelyPaused(); + } + + handlePlayerPermanentlyLeft(playerRole, characterKey, reason) { + console.log(`[GameInstance ${this.id}] Player permanently left. Role: ${playerRole}, Reason: ${reason}`); + if (this.gameState && !this.gameState.isGameOver) { + // Используем геттер playerCount + if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) { + this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game"); + } else if (this.mode === 'pvp') { + if (this.playerCount < 2) { + // Используем геттер players для поиска оставшегося + const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected); + this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id); + } + } + } else if (!this.gameState && Object.keys(this.players).length === 0) { // Используем геттер players + this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi_via_pch"); + } } _sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) { - if (!characterState || !characterState.characterKey) { - return; - } - if (!opponentCharacterKey) { - return; - } - if (!gameLogic.getRandomTaunt) { - console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`); - return; - } - if (!this.gameState) { - return; - } + if (!characterState || !characterState.characterKey) return; + if (!opponentCharacterKey) return; + if (!gameLogic.getRandomTaunt) { console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`); return; } + if (!this.gameState) return; let context = {}; let subTrigger = null; @@ -83,9 +129,7 @@ class GameInstance { } const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey); - if (!opponentFullData) { - return; - } + if (!opponentFullData) return; const tauntText = gameLogic.getRandomTaunt( characterState.characterKey, @@ -101,320 +145,25 @@ class GameInstance { } } - addPlayer(socket, chosenCharacterKey = 'elena', identifier) { - // ... (Код addPlayer без изменений из предыдущего вашего файла) ... - console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`); - const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier); - - if (existingPlayerByIdentifier) { - console.warn(`[GameInstance ${this.id}] Identifier ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`); - if (this.gameState && this.gameState.isGameOver) { - console.warn(`[GameInstance ${this.id}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`); - socket.emit('gameError', { message: 'Эта игра уже завершена.' }); - this.gameManager._cleanupGame(this.id, `rejoin_attempt_to_finished_game_${identifier}`); - return false; - } - if (existingPlayerByIdentifier.isTemporarilyDisconnected) { - return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket); - } - socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' }); - return false; - } - - if (Object.keys(this.players).length >= 2 && this.playerCount >=2) { - socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); - return false; - } - - let assignedPlayerId; - let actualCharacterKey = chosenCharacterKey || 'elena'; - - if (this.mode === 'ai') { - if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { - socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); - return false; - } - assignedPlayerId = GAME_CONFIG.PLAYER_ID; - } else { - 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 { - socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' }); - return false; - } - } - - 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]; - if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.id); } catch(e){} } - delete this.players[oldPlayerSocketIdForRole]; - } - - this.players[socket.id] = { - id: assignedPlayerId, - socket: socket, - chosenCharacterKey: actualCharacterKey, - identifier: identifier, - isTemporarilyDisconnected: false - }; - this.playerSockets[assignedPlayerId] = socket; - this.playerCount++; - socket.join(this.id); - - if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.playerCharacterKey = actualCharacterKey; - else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.opponentCharacterKey = actualCharacterKey; - - if (!this.ownerIdentifier && (this.mode === 'ai' || (this.mode === 'pvp' && assignedPlayerId === GAME_CONFIG.PLAYER_ID))) { - this.ownerIdentifier = identifier; - } - - const charData = dataUtils.getCharacterData(actualCharacterKey); - console.log(`[GameInstance ${this.id}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${this.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(`[GameInstance ${this.id}] Final removal of player ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Reason: ${reason}.`); - - if (playerInfo.socket) { - try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ } - } - - if (!playerInfo.isTemporarilyDisconnected) { - this.playerCount--; - } - - delete this.players[socketId]; - if (this.playerSockets[playerRole]?.id === socketId) { - delete this.playerSockets[playerRole]; - } - this.clearReconnectTimer(playerRole); - - console.log(`[GameInstance ${this.id}] Player ${playerIdentifier} removed. Active players now: ${this.playerCount}.`); - - if (this.gameState && !this.gameState.isGameOver) { - if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) { - this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "player_left_ai_game"); - } else if (this.mode === 'pvp') { - const remainingActivePlayer = Object.values(this.players).find(p => p.socket && p.socket.connected && !p.isTemporarilyDisconnected); - if (this.playerCount < 2) { - this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "opponent_left_pvp_game", remainingActivePlayer?.id); - } - } - } else if (!this.gameState && Object.keys(this.players).length === 0) { - this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi"); - } - } - } - handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { /* ... Код без изменений, вызывает turnTimer.pause() ... */ - console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`); - const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); - - if (!playerEntry || !playerEntry.socket) { return; } - if (this.gameState && this.gameState.isGameOver) { return; } - if (playerEntry.isTemporarilyDisconnected) { return; } - - playerEntry.isTemporarilyDisconnected = true; - this.playerCount--; - console.log(`[GameInstance ${this.id}] Player ${identifier} (role ${playerIdRole}) temp disconnected. Active: ${this.playerCount}. Starting reconnect timer.`); - - const disconnectedName = this.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`; - this.addToLog(`🔌 Игрок ${disconnectedName} отключился. Ожидание переподключения...`, GAME_CONFIG.LOG_TYPE_SYSTEM); - this.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.turnTimer.isActive() || (this.mode === 'ai' && this.turnTimer.isAiCurrentlyMakingMove) ) { - this.pausedTurnState = this.turnTimer.pause(); - console.log(`[GameInstance ${this.id}] Turn timer paused due to disconnect. State:`, 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) { - if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); - this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 }); - return; - } - this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: Math.ceil(remaining) }); - }, 1000); - - const timeoutId = setTimeout(() => { - this.clearReconnectTimer(playerIdRole); - 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) { /* ... Код без изменений, вызывает turnTimer.resume() или start() ... */ - const identifier = newSocket.userData?.userId; - console.log(`[GameInstance ${this.id}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`); - - if (this.gameState && this.gameState.isGameOver) { - newSocket.emit('gameError', { message: 'Игра уже завершена.' }); - this.gameManager._cleanupGame(this.id, `reconnect_to_finished_game_gi_${identifier}`); - return false; - } - - const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); - - if (playerEntry && playerEntry.isTemporarilyDisconnected) { - this.clearReconnectTimer(playerIdRole); - this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); - - const oldSocketId = playerEntry.socket.id; - if (this.players[oldSocketId] && oldSocketId !== newSocket.id) delete this.players[oldSocketId]; - - playerEntry.socket = newSocket; - playerEntry.isTemporarilyDisconnected = false; - this.players[newSocket.id] = playerEntry; - this.playerSockets[playerIdRole] = newSocket; - this.playerCount++; - - newSocket.join(this.id); - const reconnectedName = this.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey; - console.log(`[GameInstance ${this.id}] Player ${identifier} (${reconnectedName}) reconnected. Active: ${this.playerCount}.`); - this.addToLog(`🔌 Игрок ${reconnectedName} снова в игре!`, GAME_CONFIG.LOG_TYPE_SYSTEM); - - const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); - const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; - let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey); - const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; - - if (!this.gameState) { - if (!this.initializeGame()) { this._handleCriticalError('reconnect_no_gs_after_init_v2', 'GS null after re-init.'); return false; } - } - - newSocket.emit('gameStarted', { - gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState, - 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: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } - }); - - const otherSocket = this.playerSockets[oppRoleKey]; - const otherPlayerEntry = Object.values(this.players).find(p=> p.id === oppRoleKey); - if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { - otherSocket.emit('playerReconnected', { reconnectedPlayerId: playerIdRole, reconnectedPlayerName: reconnectedName }); - if (this.logBuffer.length > 0) otherSocket.emit('logUpdate', { log: this.consumeLogBuffer() }); - } - - if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver) { - this.broadcastGameStateUpdate(); - if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') { - this.turnTimer.resume( - this.pausedTurnState.remainingTime, - this.pausedTurnState.forPlayerRoleIsPlayer, - this.pausedTurnState.isAiCurrentlyMoving - ); - this.pausedTurnState = null; - } else { - const currentTurnIsForPlayer = this.gameState.isPlayerTurn; - const isCurrentTurnAi = this.mode === 'ai' && !currentTurnIsForPlayer; - this.turnTimer.start(currentTurnIsForPlayer, isCurrentTurnAi); - } - } - return true; - - } else if (playerEntry && !playerEntry.isTemporarilyDisconnected) { - if (playerEntry.socket.id !== newSocket.id) { - newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."}); return false; - } - if (!this.gameState) { if (!this.initializeGame()) {this._handleCriticalError('reconnect_same_socket_no_gs','GS null on same socket'); return false;} } - const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); - const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; - let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey); - const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; - newSocket.emit('gameStarted', { - gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState, - playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats, - playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities, - log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } - }); - return true; - } else { - newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена).' }); - return false; - } - } - - clearReconnectTimer(playerIdRole) { /* ... Код без изменений ... */ - if (this.reconnectTimers[playerIdRole]) { - clearTimeout(this.reconnectTimers[playerIdRole].timerId); - if (this.reconnectTimers[playerIdRole].updateIntervalId) { - clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); - } - delete this.reconnectTimers[playerIdRole]; - } - } - clearAllReconnectTimers() { /* ... Код без изменений ... */ - 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; - } - initializeGame() { + // Используем геттеры console.log(`[GameInstance ${this.id}] Initializing game state. Mode: ${this.mode}. Active players: ${this.playerCount}. Total entries: ${Object.keys(this.players).length}`); const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected); if (this.mode === 'ai') { - if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_v3', 'AI game init: Human player not found or not active.'); return false; } - this.playerCharacterKey = p1Entry.chosenCharacterKey; + if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_gi_v4', 'AI game init: Human player not found or not active.'); return false; } + if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'AI game init: Player character key not set.'); return false;} this.opponentCharacterKey = 'balard'; } else { - this.playerCharacterKey = p1Entry ? p1Entry.chosenCharacterKey : null; - this.opponentCharacterKey = p2Entry ? p2Entry.chosenCharacterKey : null; - + // Используем геттер playerCount + if (this.playerCount === 1 && p1Entry && !this.playerCharacterKey) { + this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP init (1 player): Player char key missing.'); return false; + } if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) { - console.error(`[GameInstance ${this.id}] PvP init error: playerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`); - this._handleCriticalError('init_pvp_char_key_missing_v3', `PvP init: playerCount is 2, but a charKey is missing.`); + console.error(`[GameInstance ${this.id}] PvP init error: activePlayerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`); + this._handleCriticalError('init_pvp_char_key_missing_gi_v4', `PvP init: activePlayerCount is 2, but a charKey is missing.`); return false; } } @@ -422,11 +171,11 @@ class GameInstance { const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null; const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null; - const isPlayerSlotFilledAndActive = !!playerData; - const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry)); // p2Entry будет null если его нет + const isPlayerSlotFilledAndActive = !!(playerData && p1Entry); + const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry)); if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) { - this._handleCriticalError('init_ai_data_fail_gs_v3', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false; + this._handleCriticalError('init_ai_data_fail_gs_gi_v4', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false; } this.logBuffer = []; @@ -437,19 +186,18 @@ class GameInstance { this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание Игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []), opponent: isOpponentSlotFilledAndActive ? this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) : - this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание Игрока 2...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []), - isPlayerTurn: isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive ? Math.random() < 0.5 : true, + this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: (this.mode === 'pvp' ? 'Ожидание Игрока 2...' : 'Противник AI'), maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []), + isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true, isGameOver: false, turnNumber: 1, gameMode: this.mode }; - // Не добавляем "Новая битва начинается" здесь, это будет в startGame, когда точно оба готовы console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name}. Opponent: ${this.gameState.opponent.name}. Ready for start if both active: ${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`); - return isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive; + return (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) : isPlayerSlotFilledAndActive; } - _createFighterState(roleId, baseStats, abilities) { /* ... Код без изменений ... */ + _createFighterState(roleId, baseStats, abilities) { const fighterState = { id: roleId, characterKey: baseStats.characterKey, name: baseStats.name, currentHp: baseStats.maxHp, maxHp: baseStats.maxHp, @@ -474,12 +222,11 @@ class GameInstance { console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`); return; } - // Перед стартом игры, убедимся, что gameState полностью инициализирован и содержит обоих персонажей. - // initializeGame должен был это сделать, но на всякий случай. + if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) { - console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init one last time.`); + console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init.`); if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) { - this._handleCriticalError('start_game_reinit_failed_sg_v4', 'Re-initialization before start failed or keys still missing in gameState.'); + this._handleCriticalError('start_game_reinit_failed_sg_gi_v5', 'Re-initialization before start failed or keys still missing in gameState.'); return; } } @@ -489,24 +236,22 @@ class GameInstance { const oData = dataUtils.getCharacterData(this.opponentCharacterKey); if (!pData || !oData) { - this._handleCriticalError('start_char_data_fail_sg_v5', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`); + this._handleCriticalError('start_char_data_fail_sg_gi_v6', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`); return; } - // Добавляем лог о начале битвы здесь, когда уверены, что оба игрока есть this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM); - // --- Начальные насмешки --- - // Убеждаемся, что объекты gameState.player и gameState.opponent существуют и имеют characterKey if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) { this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start'); this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start'); } else { - console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready. GSPlayer: ${this.gameState.player?.name}, GSOpponent: ${this.gameState.opponent?.name}`); + console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready.`); } const initialLog = this.consumeLogBuffer(); + // Используем геттер this.players Object.values(this.players).forEach(playerInfo => { if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) { const dataForThisClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ? @@ -533,6 +278,7 @@ class GameInstance { } processPlayerAction(identifier, actionData) { + // Используем геттер this.players const actingPlayerInfo = Object.values(this.players).find(p => p.identifier === identifier); if (!actingPlayerInfo || !actingPlayerInfo.socket) { console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return; @@ -556,11 +302,11 @@ class GameInstance { const defenderState = this.gameState[defenderRole]; if (!attackerState || !attackerState.characterKey || !defenderState || !defenderState.characterKey) { - this._handleCriticalError('action_actor_state_invalid_v3', `Attacker or Defender state/key invalid. Attacker: ${attackerState?.characterKey}, Defender: ${defenderState?.characterKey}`); return; + this._handleCriticalError('action_actor_state_invalid_gi_v4', `Attacker or Defender state/key invalid.`); return; } const attackerData = dataUtils.getCharacterData(attackerState.characterKey); const defenderData = dataUtils.getCharacterData(defenderState.characterKey); - if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_v3', 'Ошибка данных персонажа при действии.'); return; } + if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi_v4', 'Ошибка данных персонажа при действии.'); return; } let actionIsValidAndPerformed = false; @@ -568,11 +314,6 @@ class GameInstance { this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack'); gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt); actionIsValidAndPerformed = true; - // --- ИСПРАВЛЕНИЕ ДЛЯ СИЛЫ ПРИРОДЫ --- - // Логика бонуса (реген маны) теперь полностью внутри performAttack в combatLogic.js. - // GameInstance НЕ ДОЛЖЕН здесь "потреблять" эффект (обнулять turnsLeft или удалять). - // Длительность эффекта управляется в effectsLogic.js. - // --- КОНЕЦ ИСПРАВЛЕНИЯ --- } else if (actionData.actionType === 'ability' && actionData.abilityId) { const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId); if (!ability) { @@ -580,7 +321,6 @@ class GameInstance { } else { const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG); if (validityCheck.isValid) { - console.log(`[GameInstance Taunt Pre-Call] SelfCast: ${attackerState.name} uses ${ability.name} (${ability.id}) against ${defenderState.name} (${defenderState.characterKey})`); this._sayTaunt(attackerState, defenderState.characterKey, 'selfCastAbility', ability.id); attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost); gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null); @@ -605,16 +345,16 @@ class GameInstance { } } - switchTurn() { /* ... Код без изменений ... */ + switchTurn() { if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Switch turn deferred: game paused.`); return; } if (!this.gameState || this.gameState.isGameOver) { return; } if(this.turnTimer.isActive()) this.turnTimer.clear(); const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const endingTurnActorState = this.gameState[endingTurnActorRole]; - if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid', `Ending turn actor state or key invalid for role ${endingTurnActorRole}.`); return; } + if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid_gi', `Ending turn actor state or key invalid.`); return; } const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey); - if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail', `Char data missing for ${endingTurnActorState.characterKey}.`); return; } + if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Char data missing.`); return; } gameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils); gameLogic.updateBlockingStatus(endingTurnActorState); @@ -629,21 +369,24 @@ class GameInstance { const currentTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const currentTurnActorState = this.gameState[currentTurnActorRole]; - if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid', `Current turn actor state or name invalid for role ${currentTurnActorRole}.`); return; } + if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Current turn actor state or name invalid.`); return; } + + // Используем геттер this.players const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole); this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) { - console.log(`[GameInstance ${this.id}] Turn switched to ${currentTurnActorRole}, but player disconnected. Timer not started.`); + console.log(`[GameInstance ${this.id}] Turn switched to ${currentTurnActorRole}, but player ${currentTurnPlayerEntry.identifier} disconnected. Timer not started by switchTurn.`); } else { const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn; this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi); if (isNextTurnAi) setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } } - processAiTurn() { /* ... Код без изменений ... */ + + processAiTurn() { // Остается без изменений, так как использует this.gameState if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] AI turn deferred: game paused.`); return; } if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; } if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) { console.error(`[GameInstance ${this.id}] AI is not Balard!`); this.switchTurn(); return; } @@ -651,7 +394,7 @@ class GameInstance { const aiState = this.gameState.opponent; const playerState = this.gameState.player; - if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid', 'Player state invalid for AI taunt.'); return; } + if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid_gi', 'Player state invalid for AI turn.'); return; } const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this)); let actionIsValidAndPerformedForAI = false; @@ -678,8 +421,9 @@ class GameInstance { else { console.error(`[GameInstance ${this.id}] AI failed action. Forcing switch.`); setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } } - checkGameOver() { /* ... Код без изменений ... */ + checkGameOver() { // Остается без изменений, так как использует this.gameState if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true; + if (!this.gameState.isGameOver && this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) { const player = this.gameState.player; const opponent = this.gameState.opponent; const pData = dataUtils.getCharacterData(player.characterKey); const oData = dataUtils.getCharacterData(opponent.characterKey); @@ -696,44 +440,90 @@ class GameInstance { if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); - const winnerState = this.gameState[gameOverResult.winnerRole]; const loserState = this.gameState[gameOverResult.loserRole]; - if (winnerState?.characterKey && loserState?.characterKey) this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat'); + + const winnerState = this.gameState[gameOverResult.winnerRole]; + const loserState = this.gameState[gameOverResult.loserRole]; + if (winnerState?.characterKey && loserState?.characterKey) { + this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat'); + } if (loserState?.characterKey) { /* ... сюжетные логи ... */ } + console.log(`[GameInstance ${this.id}] Game over. Winner: ${gameOverResult.winnerRole || 'None'}. Reason: ${gameOverResult.reason}.`); - this.io.to(this.id).emit('gameOver', { winnerId: gameOverResult.winnerRole, reason: gameOverResult.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: loserState?.characterKey || 'unknown'}); + this.io.to(this.id).emit('gameOver', { + winnerId: gameOverResult.winnerRole, + reason: gameOverResult.reason, + finalGameState: this.gameState, + log: this.consumeLogBuffer(), + loserCharacterKey: loserState?.characterKey || 'unknown' + }); this.gameManager._cleanupGame(this.id, `game_ended_${gameOverResult.reason}`); return true; } return false; } - endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) { /* ... Код без изменений ... */ + + endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) { if (this.gameState && !this.gameState.isGameOver) { this.gameState.isGameOver = true; if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); - const actualWinnerRole = winnerIfAny !== null ? winnerIfAny : (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID); + + let actualWinnerRole = winnerIfAny; let winnerActuallyExists = false; + if (actualWinnerRole) { - const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole); - if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) winnerActuallyExists = !!this.gameState.opponent?.characterKey; - else if (winnerPlayerEntry && !winnerPlayerEntry.isTemporarilyDisconnected) winnerActuallyExists = true; + // Используем геттер this.players + const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected); + if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) { + winnerActuallyExists = !!this.gameState.opponent?.characterKey; + } else if (winnerPlayerEntry) { + winnerActuallyExists = true; + } } + + if (!winnerActuallyExists) { + actualWinnerRole = (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID); + // Используем геттер this.players + const defaultWinnerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected); + if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) { + winnerActuallyExists = !!this.gameState.opponent?.characterKey; + } else if (defaultWinnerEntry) { + winnerActuallyExists = true; + } + } + const finalWinnerRole = winnerActuallyExists ? actualWinnerRole : null; + const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, reason, finalWinnerRole, disconnectedPlayerRole); + this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); - console.log(`[GameInstance ${this.id}] Game ended: ${reason}. Winner: ${result.winnerRole || 'Нет'}.`); - this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: disconnectedCharacterKey, disconnectedCharacterName: reason === 'opponent_disconnected' || reason === 'player_left_ai_game' ? (this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined }); - this.gameManager._cleanupGame(this.id, `disconnect_game_ended_${result.reason}`); - } else if (this.gameState?.isGameOver) { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: already over.`); this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup`); } - else { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`); this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup`); } + console.log(`[GameInstance ${this.id}] Game ended by disconnect: ${reason}. Winner: ${result.winnerRole || 'Нет'}.`); + this.io.to(this.id).emit('gameOver', { + winnerId: result.winnerRole, + reason: result.reason, + finalGameState: this.gameState, + log: this.consumeLogBuffer(), + loserCharacterKey: disconnectedCharacterKey, + disconnectedCharacterName: (reason === 'opponent_disconnected' || reason === 'player_left_ai_game' || reason === 'opponent_left_pvp_game') ? + (this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined + }); + this.gameManager._cleanupGame(this.id, `disconnect_game_ended_gi_${result.reason}`); + } else if (this.gameState?.isGameOver) { + console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: already over.`); + this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup_gi`); + } else { + console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`); + this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup_gi`); + } } playerExplicitlyLeftAiGame(identifier) { if (this.mode !== 'ai' || (this.gameState && this.gameState.isGameOver)) { - console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame called, but not AI mode or game over. Identifier: ${identifier}`); - if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over`); + console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame called, but not AI mode or game over.`); + if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over_gi`); return; } + // Используем геттер this.players const playerEntry = Object.values(this.players).find(p => p.identifier === identifier); if (!playerEntry || playerEntry.id !== GAME_CONFIG.PLAYER_ID) { console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Identifier ${identifier} is not the human player or not found.`); @@ -751,32 +541,39 @@ class GameInstance { if (this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); - this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly'); + this.io.to(this.id).emit('gameOver', { + winnerId: GAME_CONFIG.OPPONENT_ID, + reason: "player_left_ai_game", + finalGameState: this.gameState, + log: this.consumeLogBuffer(), + loserCharacterKey: playerEntry.chosenCharacterKey + }); + + this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly_gi'); } playerDidSurrender(surrenderingPlayerIdentifier) { console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`); if (!this.gameState || this.gameState.isGameOver) { - if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished`); } + if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished_gi`); } console.warn(`[GameInstance ${this.id}] Surrender attempt on inactive/finished game by ${surrenderingPlayerIdentifier}.`); return; } - + // Используем геттер this.players const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier); if (!surrenderedPlayerEntry) { - console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found in this.players.`); + console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found.`); return; } - - const surrenderingPlayerRole = surrenderedPlayerEntry.id; // ОПРЕДЕЛЯЕМ ЗДЕСЬ + const surrenderingPlayerRole = surrenderedPlayerEntry.id; if (this.mode === 'ai') { if (surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID) { console.log(`[GameInstance ${this.id}] Player ${surrenderingPlayerIdentifier} "surrendered" (left) AI game.`); this.playerExplicitlyLeftAiGame(surrenderingPlayerIdentifier); } else { - console.warn(`[GameInstance ${this.id}] Surrender in AI mode from non-player (role: ${surrenderingPlayerRole}) or unexpected: ${surrenderingPlayerIdentifier}`); + console.warn(`[GameInstance ${this.id}] Surrender in AI mode from non-player (role: ${surrenderingPlayerRole}).`); } return; } @@ -809,54 +606,93 @@ class GameInstance { finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: surrenderedPlayerCharKey }); - this.gameManager._cleanupGame(this.id, "player_surrendered"); + this.gameManager._cleanupGame(this.id, "player_surrendered_gi"); } - handleTurnTimeout() { /* ... Код без изменений ... */ + handleTurnTimeout() { if (!this.gameState || this.gameState.isGameOver) return; console.log(`[GameInstance ${this.id}] Turn timeout occurred.`); const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; - const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; + + const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; let winnerActuallyExists = false; - if (this.mode === 'ai' && winnerPlayerRole === GAME_CONFIG.OPPONENT_ID) winnerActuallyExists = !!this.gameState.opponent?.characterKey; - else { const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRole && !p.isTemporarilyDisconnected); winnerActuallyExists = !!winnerEntry; } - const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRole : null, timedOutPlayerRole); + + if (this.mode === 'ai' && winnerPlayerRoleIfHuman === GAME_CONFIG.OPPONENT_ID) { + winnerActuallyExists = !!this.gameState.opponent?.characterKey; + } else { + // Используем геттер this.players + const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRoleIfHuman && !p.isTemporarilyDisconnected); + winnerActuallyExists = !!winnerEntry; + } + + const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : null, timedOutPlayerRole); + this.gameState.isGameOver = true; this.clearAllReconnectTimers(); + this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); - if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat'); + if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) { + this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat'); + } console.log(`[GameInstance ${this.id}] Turn timed out for ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Winner: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`); - this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown' }); - this.gameManager._cleanupGame(this.id, `timeout_${result.reason}`); + this.io.to(this.id).emit('gameOver', { + winnerId: result.winnerRole, + reason: result.reason, + finalGameState: this.gameState, + log: this.consumeLogBuffer(), + loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown' + }); + this.gameManager._cleanupGame(this.id, `timeout_gi_${result.reason}`); } - _handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений ... */ + + _handleCriticalError(reasonCode, logMessage) { console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`); if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true; else if (!this.gameState) this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode }; + if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); + this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM); - this.io.to(this.id).emit('gameOver', { winnerId: null, reason: `server_error_${reasonCode}`, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: 'unknown'}); - this.gameManager._cleanupGame(this.id, `critical_error_${reasonCode}`); + this.io.to(this.id).emit('gameOver', { + winnerId: null, + reason: `server_error_${reasonCode}`, + finalGameState: this.gameState, + log: this.consumeLogBuffer(), + loserCharacterKey: 'unknown' + }); + this.gameManager._cleanupGame(this.id, `critical_error_gi_${reasonCode}`); } - addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { /* ... Код без изменений ... */ - if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() }); + addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { + if (!message) return; + this.logBuffer.push({ message, type, timestamp: Date.now() }); } - consumeLogBuffer() { /* ... Код без изменений ... */ - const logs = [...this.logBuffer]; this.logBuffer = []; return logs; + + consumeLogBuffer() { + const logs = [...this.logBuffer]; + this.logBuffer = []; + return logs; } - broadcastGameStateUpdate() { /* ... Код без изменений ... */ - if (this.isGameEffectivelyPaused()) { return; } if (!this.gameState) return; + + broadcastGameStateUpdate() { + if (this.isGameEffectivelyPaused()) { return; } + if (!this.gameState) return; this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() }); } - broadcastLogUpdate() { /* ... Код без изменений ... */ + + broadcastLogUpdate() { if (this.isGameEffectivelyPaused() && this.logBuffer.some(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM)) { const systemLogs = this.logBuffer.filter(log => log.type === GAME_CONFIG.LOG_TYPE_SYSTEM); - if (systemLogs.length > 0) this.io.to(this.id).emit('logUpdate', { log: systemLogs }); - this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); return; + if (systemLogs.length > 0) { + this.io.to(this.id).emit('logUpdate', { log: systemLogs }); + } + this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); + return; + } + if (this.logBuffer.length > 0) { + this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() }); } - if (this.logBuffer.length > 0) this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() }); } } diff --git a/server/game/instance/PlayerConnectionHandler.js b/server/game/instance/PlayerConnectionHandler.js new file mode 100644 index 0000000..44e7892 --- /dev/null +++ b/server/game/instance/PlayerConnectionHandler.js @@ -0,0 +1,397 @@ +// /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 } } + 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}] Initialized.`); + } + + addPlayer(socket, chosenCharacterKey = 'elena', identifier) { + console.log(`[PCH ${this.gameId}] addPlayer attempt. 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 ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`); + if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { + console.warn(`[PCH ${this.gameId}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`); + socket.emit('gameError', { message: 'Эта игра уже завершена.' }); + this.gameInstance.gameManager._cleanupGame(this.gameId, `rejoin_attempt_to_finished_game_pch_${identifier}`); + return false; + } + if (existingPlayerByIdentifier.isTemporarilyDisconnected) { + return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket); + } + socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' }); + return false; + } + + if (Object.keys(this.players).length >= 2 && this.playerCount >=2) { + socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); + return false; + } + + let assignedPlayerId; + let actualCharacterKey = chosenCharacterKey || 'elena'; + + if (this.mode === 'ai') { + if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { + 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 { + socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' }); + return false; + } + } + + // Если для этой роли уже был игрок (например, старый сокет), удаляем его + 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]; + if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.gameId); } catch(e){} } // Убедимся, что старый сокет покинул комнату + delete this.players[oldPlayerSocketIdForRole]; + } + + + this.players[socket.id] = { + id: assignedPlayerId, + socket: socket, + chosenCharacterKey: actualCharacterKey, + identifier: identifier, + isTemporarilyDisconnected: false + }; + this.playerSockets[assignedPlayerId] = socket; + this.playerCount++; + socket.join(this.gameId); + + // Сообщаем GameInstance об установленных ключах и владельце + 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); + } + + const charData = dataUtils.getCharacterData(actualCharacterKey); // Используем dataUtils напрямую + console.log(`[PCH ${this.gameId}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${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}] Final removal of player ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Reason: ${reason}.`); + + if (playerInfo.socket) { + try { playerInfo.socket.leave(this.gameId); } catch (e) { /* ignore */ } + } + + 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}] Player ${playerIdentifier} removed. Active players now: ${this.playerCount}.`); + + // Сигнализируем GameInstance, чтобы он решил, нужно ли завершать игру + this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason); + + } else { + console.warn(`[PCH ${this.gameId}] removePlayer called for unknown socketId: ${socketId}`); + } + } + + handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { + console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`); + // Находим запись игрока по роли и идентификатору, так как сокет мог уже измениться или быть удален + const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); + + if (!playerEntry || !playerEntry.socket) { + console.warn(`[PCH ${this.gameId}] No player entry or socket found for ${identifier} (role ${playerIdRole}) during potential left.`); + return; + } + if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { + console.log(`[PCH ${this.gameId}] Game already over, not handling potential left for ${identifier}.`); + return; + } + if (playerEntry.isTemporarilyDisconnected) { + console.log(`[PCH ${this.gameId}] Player ${identifier} already marked as temp disconnected.`); + return; + } + + playerEntry.isTemporarilyDisconnected = true; + this.playerCount--; // Уменьшаем счетчик активных игроков + console.log(`[PCH ${this.gameId}] Player ${identifier} (role ${playerIdRole}) temp disconnected. Active: ${this.playerCount}. Starting reconnect timer.`); + + const disconnectedName = 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]; // Берем сокет из нашего this.playerSockets + 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.isActive() || (this.mode === 'ai' && this.gameInstance.turnTimer.isAiCurrentlyMakingMove) ) { + this.pausedTurnState = this.gameInstance.turnTimer.pause(); + console.log(`[PCH ${this.gameId}] Turn timer paused due to disconnect. State:`, JSON.stringify(this.pausedTurnState)); + } else { + this.pausedTurnState = null; // Явно сбрасываем, если таймер не был активен + } + + this.clearReconnectTimer(playerIdRole); // Очищаем старый таймер, если был + const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000; + const reconnectStartTime = Date.now(); + + // Таймер для обновления UI клиента + const updateInterval = setInterval(() => { + const remaining = reconnectDuration - (Date.now() - reconnectStartTime); + if (remaining <= 0) { // Если основной таймаут уже сработал или время вышло + if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); + 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(() => { + this.clearReconnectTimer(playerIdRole); // Очищаем таймеры (включая updateInterval) + const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); + if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) { + // Передаем socket.id из записи, а не старый socketId, который мог быть от предыдущего сокета + 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 ${this.gameId}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`); + + if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { + newSocket.emit('gameError', { message: 'Игра уже завершена.' }); + this.gameInstance.gameManager._cleanupGame(this.gameId, `reconnect_to_finished_game_pch_${identifier}`); + return false; + } + + // Находим запись игрока по роли и идентификатору + const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); + + if (playerEntry && playerEntry.isTemporarilyDisconnected) { + this.clearReconnectTimer(playerIdRole); + this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); // Сигнал, что таймер остановлен + + // Удаляем старую запись по socket.id, если сокет действительно новый + const oldSocketId = playerEntry.socket.id; + if (this.players[oldSocketId] && oldSocketId !== newSocket.id) { + delete this.players[oldSocketId]; + } + + // Обновляем запись игрока + playerEntry.socket = newSocket; + playerEntry.isTemporarilyDisconnected = false; + this.players[newSocket.id] = playerEntry; // Добавляем/обновляем запись с новым socket.id + this.playerSockets[playerIdRole] = newSocket; // Обновляем активный сокет для роли + this.playerCount++; // Восстанавливаем счетчик активных игроков + + newSocket.join(this.gameId); + const reconnectedName = this.gameInstance.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey; + console.log(`[PCH ${this.gameId}] Player ${identifier} (${reconnectedName}) reconnected. Active: ${this.playerCount}.`); + this.gameInstance.addToLog(`🔌 Игрок ${reconnectedName} снова в игре!`, GAME_CONFIG.LOG_TYPE_SYSTEM); + + 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 нет (маловероятно при реконнекте в активную игру, но возможно если это был первый игрок PvP) + // GameInstance должен сам решить, нужно ли ему initializeGame() + if (!this.gameInstance.gameState) { + // Пытаемся инициализировать игру, если она не была инициализирована + // Это важно, если первый игрок в PvP отключался до подключения второго + if (!this.gameInstance.initializeGame()) { + this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch', 'PCH: GS null after re-init on reconnect.'); + return false; + } + } + + + newSocket.emit('gameStarted', { + gameId: this.gameId, + yourPlayerId: playerIdRole, + initialGameState: this.gameInstance.gameState, // Отправляем текущее состояние + 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: this.gameInstance.consumeLogBuffer(), + clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига + }); + + // Уведомляем другого игрока + const otherSocket = this.playerSockets[oppRoleKey]; + const otherPlayerEntry = Object.values(this.players).find(p=> p.id === oppRoleKey); + if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { + otherSocket.emit('playerReconnected', { + reconnectedPlayerId: playerIdRole, + reconnectedPlayerName: reconnectedName + }); + if (this.gameInstance.logBuffer.length > 0) { // Отправляем накопившиеся логи, если есть + otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() }); + } + } + + // Если игра не на "эффективной" паузе и не закончена, возобновляем игру + if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) { + this.gameInstance.broadcastGameStateUpdate(); // Обновляем состояние для всех + if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') { + this.gameInstance.turnTimer.resume( + this.pausedTurnState.remainingTime, + this.pausedTurnState.forPlayerRoleIsPlayer, + this.pausedTurnState.isAiCurrentlyMoving + ); + this.pausedTurnState = null; // Сбрасываем сохраненное состояние таймера + } else { + // Если pausedTurnState нет, значит, таймер не был активен или это первый ход + // GameInstance.startGame или switchTurn должны запустить таймер корректно + // Но если это реконнект в середину игры, где ход уже чей-то, нужно запустить таймер + const currentTurnIsForPlayer = this.gameInstance.gameState.isPlayerTurn; + const isCurrentTurnAi = this.mode === 'ai' && !currentTurnIsForPlayer; + this.gameInstance.turnTimer.start(currentTurnIsForPlayer, isCurrentTurnAi); + } + } + return true; + + } else if (playerEntry && !playerEntry.isTemporarilyDisconnected) { + // Игрок уже был подключен и не был отмечен как isTemporarilyDisconnected + // Это может быть попытка открыть игру в новой вкладке или "обновить сессию" + if (playerEntry.socket.id !== newSocket.id) { + newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."}); + return false; // Не позволяем подключиться с нового сокета, если старый активен + } + // Если это тот же сокет (например, клиент запросил состояние), просто отправляем ему данные + if (!this.gameInstance.gameState) { // На всякий случай, если gameState вдруг нет + if (!this.gameInstance.initializeGame()) { + this.gameInstance._handleCriticalError('reconnect_same_socket_no_gs_pch','PCH: GS null on same socket reconnect.'); + return false; + } + } + const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); + const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; + 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; + + newSocket.emit('gameStarted', { + gameId: this.gameId, + yourPlayerId: playerIdRole, + initialGameState: this.gameInstance.gameState, + playerBaseStats: pData?.baseStats, + opponentBaseStats: oData?.baseStats, // Могут быть неполными, если оппонент еще не подключился + playerAbilities: pData?.abilities, + opponentAbilities: oData?.abilities, + log: this.gameInstance.consumeLogBuffer(), + clientConfig: { ...GAME_CONFIG } + }); + return true; + } else { + // Запись игрока не найдена или он не был помечен как isTemporarilyDisconnected, но сокет новый. + // Это может быть попытка реконнекта к игре, из которой игрок был уже удален (например, по таймауту). + newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена или сессия устарела).' }); + return false; + } + } + + clearReconnectTimer(playerIdRole) { + if (this.reconnectTimers[playerIdRole]) { + clearTimeout(this.reconnectTimers[playerIdRole].timerId); + if (this.reconnectTimers[playerIdRole].updateIntervalId) { + clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); + } + delete this.reconnectTimers[playerIdRole]; + console.log(`[PCH ${this.gameId}] Cleared reconnect timer for role ${playerIdRole}.`); + } + } + + clearAllReconnectTimers() { + console.log(`[PCH ${this.gameId}] Clearing ALL reconnect timers.`); + for (const roleId in this.reconnectTimers) { + this.clearReconnectTimer(roleId); + } + } + + isGameEffectivelyPaused() { + if (this.mode === 'pvp') { + // Если игроков меньше 2, И есть хотя бы один игрок в this.players (ожидающий или в процессе дисконнекта) + 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') { + // В AI режиме игра на паузе, если единственный человек-игрок временно отключен + const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); + return humanPlayer?.isTemporarilyDisconnected ?? false; // Если игрока нет, не на паузе. Если есть - зависит от его состояния. + } + return false; // В остальных случаях игра не считается на паузе из-за дисконнектов + } + + // Вспомогательный метод для получения информации о всех игроках (может пригодиться GameInstance) + getAllPlayersInfo() { + return { ...this.players }; + } + + // Вспомогательный метод для получения сокетов (может пригодиться GameInstance) + getPlayerSockets() { + return { ...this.playerSockets }; + } +} + +module.exports = PlayerConnectionHandler; \ No newline at end of file