// /server_modules/gameManager.js const { v4: uuidv4 } = require('uuid'); const GameInstance = require('./gameInstance'); const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант class GameManager { constructor(io) { this.io = io; this.games = {}; // { gameId: GameInstance } this.socketToGame = {}; // { socket.id: gameId } this.pendingPvPGames = []; // [gameId] - ID игр, ожидающих второго игрока в PvP this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId } - для отслеживания созданных ожидающих игр } _removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) { const keyToUse = identifier || currentSocketId; const oldPendingGameId = this.userToPendingGame[keyToUse]; if (oldPendingGameId && oldPendingGameId !== excludeGameId) { const gameToRemove = this.games[oldPendingGameId]; if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) { const playersInOldGame = Object.values(gameToRemove.players); const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId; const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier; if (isOwnerBySocket || isOwnerByUserId) { console.log(`[GameManager] Пользователь ${keyToUse} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`); delete this.games[oldPendingGameId]; const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId); if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1); if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) { delete this.socketToGame[playersInOldGame[0].socket.id]; } delete this.userToPendingGame[keyToUse]; this.broadcastAvailablePvPGames(); } } else if (oldPendingGameId === excludeGameId) { // Это та же игра, к которой игрок присоединяется, ничего не делаем } else { delete this.userToPendingGame[keyToUse]; } } } createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) { const identifier = userId || socket.id; this._removePreviousPendingGames(socket.id, identifier); const gameId = uuidv4(); const game = new GameInstance(gameId, this.io, mode); if (userId) game.ownerUserId = userId; this.games[gameId] = game; // В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena'; if (game.addPlayer(socket, charKeyForInstance)) { this.socketToGame[socket.id] = gameId; console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.userData?.username || socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`); const assignedPlayerId = game.players[socket.id]?.id; if (!assignedPlayerId) { delete this.games[gameId]; if(this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id]; socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось назначить ID игрока).' }); return; } socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId }); if (mode === 'pvp') { if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId); this.userToPendingGame[identifier] = gameId; this.broadcastAvailablePvPGames(); } } else { delete this.games[gameId]; if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id]; // Сообщение об ошибке отправляется из game.addPlayer } } joinGame(socket, gameId, userId = null) { const identifier = userId || socket.id; 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.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;} this._removePreviousPendingGames(socket.id, identifier, gameId); // addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого if (game.addPlayer(socket)) { this.socketToGame[socket.id] = gameId; console.log(`[GameManager] Игрок ${socket.userData?.username || socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`); const gameIndex = this.pendingPvPGames.indexOf(gameId); if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1); if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) { delete this.userToPendingGame[game.ownerUserId]; } else { const firstPlayerSocketId = Object.keys(game.players).find(sId => game.players[sId].id === GAME_CONFIG.PLAYER_ID && game.players[sId].socket.id !== socket.id); if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) { delete this.userToPendingGame[firstPlayerSocketId]; } } this.broadcastAvailablePvPGames(); } else { // Сообщение об ошибке отправляется из game.addPlayer } } findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', userId = null) { const identifier = userId || socket.id; this._removePreviousPendingGames(socket.id, identifier); let gameIdToJoin = null; // Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору) const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena'; // Сначала ищем игру, где первый игрок выбрал "зеркального" персонажа for (const id of this.pendingPvPGames) { const pendingGame = this.games[id]; if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') { const firstPlayerInfo = Object.values(pendingGame.players)[0]; const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id); if (isMyOwnGame) continue; if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) { gameIdToJoin = id; break; } } } // Если не нашли с предпочтительным оппонентом, ищем любую свободную (не нашу) if (!gameIdToJoin && this.pendingPvPGames.length > 0) { for (const id of this.pendingPvPGames) { const pendingGame = this.games[id]; if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') { const firstPlayerInfo = Object.values(pendingGame.players)[0]; const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id); if (isMyOwnGame) continue; gameIdToJoin = id; break; } } } if (gameIdToJoin) { // Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку. this.joinGame(socket, gameIdToJoin, userId); } else { // Если свободных игр нет, создаем новую с выбранным персонажем this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, userId); // Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения socket.emit('noPendingGamesFound', { message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.', gameId: this.userToPendingGame[identifier], // ID только что созданной игры yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID }); } } handlePlayerAction(socketId, actionData) { const gameIdFromSocket = this.socketToGame[socketId]; const game = this.games[gameIdFromSocket]; if (game) { game.processPlayerAction(socketId, actionData); } else { const playerSocket = this.io.sockets.sockets.get(socketId); if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна для этого действия.' }); } } handleDisconnect(socketId, userId = null) { const identifier = userId || socketId; const gameId = this.socketToGame[socketId]; if (gameId && this.games[gameId]) { const game = this.games[gameId]; const playerInfo = game.players[socketId]; const username = playerInfo?.socket?.userData?.username || socketId; console.log(`[GameManager] Игрок ${username} (socket: ${socketId}, userId: ${userId}) отключился от игры ${gameId}.`); game.removePlayer(socketId); if (game.playerCount === 0) { console.log(`[GameManager] Игра ${gameId} пуста и будет удалена (после дисконнекта).`); delete this.games[gameId]; const gameIndexPending = this.pendingPvPGames.indexOf(gameId); if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1); for (const key in this.userToPendingGame) { if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key]; } this.broadcastAvailablePvPGames(); } else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { if (!this.pendingPvPGames.includes(gameId)) { this.pendingPvPGames.push(gameId); } const remainingPlayerSocketId = Object.keys(game.players)[0]; const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket; const remainingUserId = remainingPlayerSocket?.userData?.userId; const newIdentifier = remainingUserId || remainingPlayerSocketId; game.ownerUserId = remainingUserId; this.userToPendingGame[newIdentifier] = gameId; if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) { delete this.userToPendingGame[identifier]; } console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`); this.broadcastAvailablePvPGames(); } } else { const pendingGameIdToRemove = this.userToPendingGame[identifier]; if (pendingGameIdToRemove && this.games[pendingGameIdToRemove] && this.games[pendingGameIdToRemove].playerCount === 1) { console.log(`[GameManager] Игрок ${socketId} (identifier: ${identifier}) отключился, удаляем его ожидающую игру ${pendingGameIdToRemove}`); delete this.games[pendingGameIdToRemove]; const idx = this.pendingPvPGames.indexOf(pendingGameIdToRemove); if (idx > -1) this.pendingPvPGames.splice(idx, 1); delete this.userToPendingGame[identifier]; this.broadcastAvailablePvPGames(); } } delete this.socketToGame[socketId]; } getAvailablePvPGamesListForClient() { return this.pendingPvPGames .map(gameId => { const game = this.games[gameId]; if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { let firstPlayerUsername = 'Игрок'; let firstPlayerCharacterName = ''; if (game.players && Object.keys(game.players).length > 0) { const firstPlayerSocketId = Object.keys(game.players)[0]; const firstPlayerInfo = game.players[firstPlayerSocketId]; if (firstPlayerInfo) { if (firstPlayerInfo.socket?.userData?.username) { firstPlayerUsername = firstPlayerInfo.socket.userData.username; } const charKey = firstPlayerInfo.chosenCharacterKey; if (charKey) { let charBaseStats; if (charKey === 'elena') { charBaseStats = gameData.playerBaseStats; } else if (charKey === 'almagest') { charBaseStats = gameData.almagestBaseStats; } // Баларда не должно быть в pending PvP как создателя if (charBaseStats && charBaseStats.name) { firstPlayerCharacterName = charBaseStats.name; } else { console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`); firstPlayerCharacterName = charKey; // В крайнем случае ключ } } else { console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}`); } } } let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`; if (firstPlayerCharacterName) { statusString += ` за ${firstPlayerCharacterName}`; } statusString += `)`; return { id: gameId, status: statusString }; } return null; }) .filter(info => info !== null); } broadcastAvailablePvPGames() { this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient()); } getActiveGamesList() { // Для отладки на сервере return Object.values(this.games).map(game => { let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey === 'elena' ? 'playerBaseStats' : (game.playerCharacterKey === 'almagest' ? 'almagestBaseStats' : null)]?.name : 'N/A'); let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey === 'elena' ? 'playerBaseStats' : (game.opponentCharacterKey === 'almagest' ? 'almagestBaseStats' : (game.opponentCharacterKey === 'balard' ? 'opponentBaseStats' : null))]?.name : 'N/A'); if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey && game.gameState && !game.gameState.isGameOver) { opponentSlotChar = 'Ожидание...'; } return { id: game.id.substring(0,8), mode: game.mode, playerCount: game.playerCount, isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A', playerSlot: playerSlotChar, opponentSlot: opponentSlotChar, ownerUserId: game.ownerUserId || 'N/A', pending: this.pendingPvPGames.includes(game.id) }; }); } } module.exports = GameManager;