// /server/game/GameManager.js const { v4: uuidv4 } = require('uuid'); const GameInstance = require('./instance/GameInstance'); const dataUtils = require('../data/dataUtils'); const GAME_CONFIG = require('../core/config'); class GameManager { constructor(io) { this.io = io; this.games = {}; this.userIdentifierToGameId = {}; this.pendingPvPGames = []; console.log("[GameManager] Initialized."); } _removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) { const oldPendingGameId = this.userIdentifierToGameId[identifier]; if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) { const gameToRemove = this.games[oldPendingGameId]; if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) { const oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) { console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`); this._cleanupGame(oldPendingGameId, 'replaced_by_new_game'); } } } } createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) { this._removePreviousPendingGames(socket.id, identifier); if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) { socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } const gameId = uuidv4(); const game = new GameInstance(gameId, this.io, mode, this); game.ownerIdentifier = identifier; // Устанавливаем владельца игры this.games[gameId] = game; const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena'; if (game.addPlayer(socket, charKeyForInstance, identifier)) { this.userIdentifierToGameId[identifier] = gameId; console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (выбран: ${charKeyForInstance})`); const assignedPlayerId = game.players[socket.id]?.id; if (!assignedPlayerId) { this._cleanupGame(gameId, 'player_add_failed_no_role'); socket.emit('gameError', { message: 'Ошибка сервера при создании игры (роль).' }); return; } socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId }); if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) { const isInitialized = game.initializeGame(); if (isInitialized) game.startGame(); else this._cleanupGame(gameId, 'initialization_failed_on_create'); if (game.mode === 'pvp' && game.playerCount === 2) { // Если PvP заполнилась const idx = this.pendingPvPGames.indexOf(gameId); if (idx > -1) this.pendingPvPGames.splice(idx, 1); this.broadcastAvailablePvPGames(); } } else if (mode === 'pvp' && game.playerCount === 1) { if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId); game.initializeGame(); socket.emit('waitingForOpponent'); this.broadcastAvailablePvPGames(); } } else { this._cleanupGame(gameId, 'player_add_failed_instance'); } } joinGame(socket, gameId, identifier) { // identifier - это userId присоединяющегося const game = this.games[gameId]; if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; } if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; } if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } // === ИЗМЕНЕНИЕ: Запрет присоединения к своей же игре === if (game.ownerIdentifier === identifier) { socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете.' }); // Можно отправить состояние этой игры, если она действительно ожидает this.handleRequestGameState(socket, identifier); return; } // === КОНЕЦ ИЗМЕНЕНИЯ === if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameId) { socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' }); this.handleRequestGameState(socket, identifier); return; } // Проверка на случай, если игрок пытается присоединиться к игре, где он уже есть (хотя ownerIdentifier проверка выше это частично покрывает для создателя) const existingPlayerInThisGame = Object.values(game.players).find(p => p.identifier === identifier); if (existingPlayerInThisGame) { socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' }); this.handleRequestGameState(socket, identifier); // Отправляем состояние игры return; } this._removePreviousPendingGames(socket.id, identifier, gameId); if (game.addPlayer(socket, null, identifier)) { this.userIdentifierToGameId[identifier] = gameId; console.log(`[GameManager] Игрок ${identifier} присоединился к PvP игре ${gameId}`); if (game.mode === 'pvp' && game.playerCount === 2) { const isInitialized = game.initializeGame(); if (isInitialized) game.startGame(); else this._cleanupGame(gameId, 'initialization_failed_on_join'); const idx = this.pendingPvPGames.indexOf(gameId); if (idx > -1) this.pendingPvPGames.splice(idx, 1); this.broadcastAvailablePvPGames(); } } } findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { this._removePreviousPendingGames(socket.id, identifier); if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) { socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } let gameIdToJoin = null; const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena'; // Ищем игру, созданную НЕ текущим пользователем for (const id of [...this.pendingPvPGames]) { const pendingGame = this.games[id]; // === ИЗМЕНЕНИЕ: Убеждаемся, что не присоединяемся к игре, которую сами создали и ожидаем === if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) { // === КОНЕЦ ИЗМЕНЕНИЯ === const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) { gameIdToJoin = id; break; } if (!gameIdToJoin) gameIdToJoin = id; // Берем первую подходящую, если нет с нужным персонажем } } if (gameIdToJoin) { this.joinGame(socket, gameIdToJoin, identifier); } else { this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); // Сообщение о создании новой игры отправляется из createGame/initializeGame/startGame } } handlePlayerAction(identifier, actionData) { const gameId = this.userIdentifierToGameId[identifier]; const game = this.games[gameId]; if (game) { const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); const currentSocketId = playerInfo?.socket?.id; if (playerInfo && currentSocketId) { const actualSocket = this.io.sockets.sockets.get(currentSocketId); if (actualSocket?.connected) game.processPlayerAction(currentSocketId, actionData); else console.warn(`[GameManager] Игрок ${identifier}: действие, но сокет ${currentSocketId} отключен.`); } else { console.warn(`[GameManager] Игрок ${identifier}: действие для игры ${gameId}, но не найден в game.players.`); delete this.userIdentifierToGameId[identifier]; const s = this.io.sockets.sockets.get(identifier) || playerInfo?.socket; if (s) s.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока).' }); } } else { console.warn(`[GameManager] Игрок ${identifier}: действие, но игра ${gameId} не найдена.`); delete this.userIdentifierToGameId[identifier]; const s = this.io.sockets.sockets.get(identifier); if (s) s.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена.' }); } } handleDisconnect(socketId, identifier) { const gameId = this.userIdentifierToGameId[identifier]; const game = this.games[gameId]; if (game) { // Ищем игрока по ИДЕНТИФИКАТОРУ, так как сокет мог уже обновиться при переподключении const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfo) { // Проверяем, действительно ли отключается АКТУАЛЬНЫЙ сокет этого игрока if (playerInfo.socket.id === socketId) { console.log(`[GameManager] Актуальный сокет ${socketId} игрока ${identifier} отключился из игры ${gameId}.`); const dPlayerRole = playerInfo.id; const dCharKey = playerInfo.chosenCharacterKey; game.removePlayer(socketId); // Удаляем именно этот сокет из игры if (game.playerCount === 0) { this._cleanupGame(gameId, 'all_players_disconnected'); } else if (game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) { game.endGameDueToDisconnect(socketId, dPlayerRole, dCharKey); } else if (game.mode === 'ai' && game.playerCount === 0 && game.gameState && !game.gameState.isGameOver) { game.endGameDueToDisconnect(socketId, dPlayerRole, dCharKey); // Завершаем AI игру, если игрок ушел } // Если игра уже была isGameOver, _cleanupGame был вызван ранее. // userIdentifierToGameId[identifier] для отключившегося игрока УДАЛЯЕТСЯ здесь, // чтобы он мог начать новую игру или переподключиться. delete this.userIdentifierToGameId[identifier]; } else { // Отключился старый сокет (socketId), но у игрока (identifier) уже новый активный сокет. // Ничего не делаем с игрой, так как игрок по-прежнему в ней с новым сокетом. // Просто логируем, что старый сокет отвалился. console.log(`[GameManager] Отключился старый сокет ${socketId} для игрока ${identifier}, который уже переподключился с сокетом ${playerInfo.socket.id} в игре ${gameId}.`); // Связь userIdentifierToGameId[identifier] остается, так как он все еще в игре. } } else { // Игрока с таким identifier нет в этой игре. // Это может случиться, если игра была очищена до того, как пришло событие disconnect. // console.log(`[GameManager] Отключившийся сокет ${socketId} (identifier: ${identifier}) не найден в активных игроках игры ${gameId} (возможно, игра уже очищена).`); delete this.userIdentifierToGameId[identifier]; // На всякий случай. } } else { // console.log(`[GameManager] Отключился сокет ${socketId} (identifier: ${identifier}). Активная игра не найдена по идентификатору.`); delete this.userIdentifierToGameId[identifier]; } } _cleanupGame(gameId, reason = 'unknown') { const game = this.games[gameId]; if (!game) return false; console.log(`[GameManager] Очистка игры ${gameId} (Причина: ${reason}).`); if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear(); Object.values(game.players).forEach(pInfo => { if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) { delete this.userIdentifierToGameId[pInfo.identifier]; } }); if(game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId){ delete this.userIdentifierToGameId[game.ownerIdentifier]; } const pendingIdx = this.pendingPvPGames.indexOf(gameId); if (pendingIdx > -1) this.pendingPvPGames.splice(pendingIdx, 1); delete this.games[gameId]; this.broadcastAvailablePvPGames(); return true; } getAvailablePvPGamesListForClient() { return this.pendingPvPGames.map(gameId => { const game = this.games[gameId]; if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) { const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); let p1Username = 'Игрок'; let p1CharName = ''; let ownerId = game.ownerIdentifier; // === ИЗМЕНЕНИЕ: Получаем ownerId === if (p1Info) { p1Username = p1Info.socket?.userData?.username || `User#${String(p1Info.identifier).substring(0,4)}`; const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey); p1CharName = charData?.name || p1Info.chosenCharacterKey; } return { id: gameId, status: `Ожидает (Создал: ${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId // === ИЗМЕНЕНИЕ: Отправляем ownerIdentifier клиенту === }; } return null; }).filter(info => info !== null); } broadcastAvailablePvPGames() { this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient()); } handleRequestGameState(socket, identifier) { const gameId = this.userIdentifierToGameId[identifier]; const game = gameId ? this.games[gameId] : null; if (game) { const playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfoInGameInstance) { if (game.gameState?.isGameOver) { delete this.userIdentifierToGameId[identifier]; socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' }); return; } console.log(`[GameManager] Восстановление игры ${gameId} для ${identifier}. Новый сокет ${socket.id}.`); const oldSocketId = playerInfoInGameInstance.socket?.id; // Добавил ?. на случай если сокета нет if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) { delete game.players[oldSocketId]; if(game.playerSockets[playerInfoInGameInstance.id]?.id === oldSocketId) { delete game.playerSockets[playerInfoInGameInstance.id]; } } playerInfoInGameInstance.socket = socket; game.players[socket.id] = playerInfoInGameInstance; game.playerSockets[playerInfoInGameInstance.id] = socket; socket.join(game.id); const pCharKey = playerInfoInGameInstance.chosenCharacterKey; const pData = dataUtils.getCharacterData(pCharKey); const opponentRole = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const oCharKey = game.gameState?.[opponentRole]?.characterKey || (playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey); const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; // oData может быть null, если оппонента нет if (pData && (oData || !game.opponentCharacterKey) && game.gameState) { socket.emit('gameStarted', { gameId: game.id, yourPlayerId: playerInfoInGameInstance.id, initialGameState: game.gameState, playerBaseStats: pData.baseStats, opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1}, // Заглушка если оппонента нет playerAbilities: pData.abilities, opponentAbilities: oData?.abilities || [], log: game.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } }); if(game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier) socket.emit('waitingForOpponent'); if (!game.gameState.isGameOver && game.turnTimer?.start) { game.turnTimer.start(game.gameState.isPlayerTurn, (game.mode === 'ai' && !game.gameState.isPlayerTurn)); } } else { this._handleGameRecoveryError(socket, gameId, identifier, 'data_load_fail_reconnect'); } } else { this._handleGameRecoveryError(socket, gameId, identifier, 'player_not_in_instance_reconnect'); } } else { socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' }); } } _handleGameRecoveryError(socket, gameId, identifier, reasonCode) { console.error(`[GameManager] Ошибка восстановления игры ${gameId} для ${identifier} (причина: ${reasonCode}).`); socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' }); this._cleanupGame(gameId, `recovery_error_${reasonCode}`); socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' }); } } module.exports = GameManager;