792 lines
67 KiB
JavaScript
792 lines
67 KiB
JavaScript
// /server_modules/gameManager.js
|
||
const { v4: uuidv4 } = require('uuid'); // Убедитесь, что uuidv4 установлен: npm install uuid
|
||
const GameInstance = require('./gameInstance'); // Убедитесь, что GameInstance экспортируется из gameInstance.js
|
||
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient и данных персонажей
|
||
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
||
|
||
class GameManager {
|
||
constructor(io) {
|
||
this.io = io; // Ссылка на Socket.IO сервер для широковещательных рассылок
|
||
this.games = {}; // { gameId: GameInstance } - Все активные или ожидающие игры
|
||
this.userIdentifierToGameId = {}; // { userId|socketId: gameId } - Какому пользователю какая игра соответствует (более стабильно, чем socket.id)
|
||
this.pendingPvPGames = []; // [gameId] - ID PvP игр, ожидающих второго игрока
|
||
|
||
// Навешиваем обработчик события 'gameOver' на Socket.IO сервер
|
||
// Это событие исходит от экземпляра GameInstance при завершении игры (по HP или дисконнекту)
|
||
// Мы слушаем его здесь, чтобы GameManager мог очистить ссылки.
|
||
// Примечание: Это событие отправляется всем в комнате игры. GameManager слушает его через io.sockets.sockets.on,
|
||
// но удобнее слушать его на уровне io, если возможно, или добавить специальный emit из GameInstance.
|
||
// Текущая архитектура (GameInstance напрямую вызывает io.to(...).emit('gameOver', ...)) уже рабочая.
|
||
// GameManager сам должен отреагировать на завершение, проверяя gameState.isGameOver после каждого действия/хода.
|
||
// Или GameInstance должен вызвать специальный метод GameManager при gameOver.
|
||
// Давайте сделаем GameInstance вызывать метод GameManager при gameOver.
|
||
}
|
||
|
||
/**
|
||
* Удаляет предыдущую ожидающую игру пользователя, если таковая существует.
|
||
* Это предотвращает создание множества пустых игр одним пользователем.
|
||
* @param {string} currentSocketId - ID текущего сокета.
|
||
* @param {string|number} identifier - userId или socketId пользователя.
|
||
* @param {string|null} excludeGameId - ID игры, которую НЕ нужно удалять (например, если пользователь присоединяется к своей же игре).
|
||
*/
|
||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||
// Ищем игру по идентификатору пользователя
|
||
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||
|
||
// Проверяем, что нашли игру, она не исключена, и она все еще существует в списке игр
|
||
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||
const gameToRemove = this.games[oldPendingGameId];
|
||
// Проверяем, что игра является ожидающей PvP игрой с одним игроком
|
||
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||
// Проверяем, что этот пользователь является владельцем этой ожидающей игры
|
||
// Владелец в pendingPvPGames - это всегда тот, кто ее создал (первый игрок в слоте PLAYER_ID)
|
||
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');
|
||
|
||
// Оповещаем клиентов об обновленном списке игр (уже внутри _cleanupGame)
|
||
// this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
// Если игра не соответствует критериям ожидающей игры, но идентификатор был связан с ней,
|
||
// это может означать, что игра уже началась или была завершена.
|
||
// Просто очищаем ссылку, если она не ведет в исключенную игру.
|
||
// Идентификатор должен был быть очищен из userIdentifierToGameId при старте или завершении игры.
|
||
// На всякий случай убеждаемся, что мы не удаляем ссылку на игру, к которой только что присоединились.
|
||
if (this.userIdentifierToGameId[identifier] !== excludeGameId) {
|
||
console.warn(`[GameManager] Удаление потенциально некорректной ссылки userIdentifierToGameId[${identifier}] на игру ${oldPendingGameId}.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
// Если oldPendingGameId не найдена, или она равна excludeGameId, ничего не делаем.
|
||
}
|
||
|
||
|
||
/**
|
||
* Создает новую игру.
|
||
* @param {object} socket - Сокет игрока, создающего игру.
|
||
* @param {string} [mode='ai'] - Режим игры ('ai' или 'pvp').
|
||
* @param {string} [chosenCharacterKey='elena'] - Выбранный персонаж для первого игрока в PvP.
|
||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||
*/
|
||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
|
||
// Удаляем старые ожидающие игры этого пользователя, прежде чем создавать новую
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
|
||
// Проверяем, не находится ли пользователь уже в какой-то игре (активной или ожидающей)
|
||
// Проверяем наличие ссылки на игру по идентификатору пользователя
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на создание.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||
// Можно попробовать отправить состояние текущей игры пользователю
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
|
||
|
||
const gameId = uuidv4(); // Генерируем уникальный ID для игры
|
||
// Передаем ссылку на GameManager в GameInstance, чтобы он мог вызвать _notifyGameEnded
|
||
const game = new GameInstance(gameId, this.io, mode, this); // <-- ПЕРЕДАЕМ GameManager
|
||
game.ownerIdentifier = identifier; // Сохраняем идентификатор создателя
|
||
this.games[gameId] = game; // Добавляем игру в список активных игр
|
||
|
||
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали при создании
|
||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||
|
||
// Добавляем игрока в созданный экземпляр игры, передавая идентификатор
|
||
// GameInstance.addPlayer принимает socket, chosenCharacterKey, identifier
|
||
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
|
||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (сокет: ${socket.id}, выбран: ${charKeyForInstance})`);
|
||
|
||
// Уведомляем игрока, что игра создана, и передаем его технический ID слота
|
||
const assignedPlayerId = game.players[socket.id]?.id; // ID слота все еще берем из playerInfo по socket.id
|
||
if (!assignedPlayerId) {
|
||
// Если по какой-то причине не удалось назначить ID игрока, удаляем игру и отправляем ошибку
|
||
// Используем централизованную функцию очистки
|
||
this._cleanupGame(gameId, 'player_add_failed');
|
||
console.error(`[GameManager] Ошибка при создании игры ${gameId}: Не удалось назначить ID игрока сокету ${socket.id} (идентификатор ${identifier}).`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры.' });
|
||
return;
|
||
}
|
||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||
|
||
|
||
// --- Логика старта игры ---
|
||
// Если игра AI и теперь с 1 игроком, или PvP и теперь с 2 игроками, запускаем ее немедленно
|
||
if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) {
|
||
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||
game.startGame(); // Запускаем игру
|
||
} else {
|
||
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||
// initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
|
||
// Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
|
||
this._cleanupGame(gameId, 'initialization_failed');
|
||
}
|
||
|
||
// Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
|
||
// Идентификаторы игроков остаются связанными с игрой в userIdentifierToGameId до ее завершения.
|
||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||
// Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
|
||
this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
|
||
}
|
||
|
||
|
||
} else if (mode === 'pvp' && game.playerCount === 1) {
|
||
// Если игра PvP и ожидает второго игрока, добавляем ее в список ожидающих
|
||
if (!this.pendingPvPGames.includes(gameId)) {
|
||
this.pendingPvPGames.push(gameId); // Добавляем ID игры в список ожидающих
|
||
}
|
||
// userIdentifierToGameId для создателя уже установлен выше
|
||
|
||
// Частичная инициализация gameState для отображения Player 1 на UI ожидания
|
||
// initializeGame вызывается при playerCount === 1 в GameInstance
|
||
game.initializeGame();
|
||
|
||
this.broadcastAvailablePvPGames(); // Обновляем список у всех
|
||
}
|
||
// --- КОНЕЦ Логики старта игры ---
|
||
|
||
|
||
} else {
|
||
// Если не удалось добавить игрока в GameInstance (например, уже 2 игрока - хотя проверили выше), удаляем игру
|
||
// Используем централизованную функцию очистки
|
||
this._cleanupGame(gameId, 'player_add_failed');
|
||
// GameInstance.addPlayer уже отправил ошибку клиенту
|
||
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}. Игра удалена.`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Присоединяет игрока к существующей игре по ID.
|
||
* @param {object} socket - Сокет игрока.
|
||
* @param {string} gameId - ID игры, к которой нужно присоединиться.
|
||
* @param {string|number} identifier - ID пользователя (userId).
|
||
*/
|
||
joinGame(socket, gameId, identifier) { // В joinGame всегда передается userId, т.к. PvP требует логина
|
||
const game = this.games[gameId]; // Находим игру по ID
|
||
|
||
// Проверки перед присоединением
|
||
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 (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]] && this.userIdentifierToGameId[identifier] !== gameId) {
|
||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на присоединение.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||
this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
|
||
return;
|
||
}
|
||
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;} // Проверка на повторное присоединение по текущему сокету (хотя userIdentifierToGameId должен это предотвратить)
|
||
|
||
// Удаляем старые ожидающие игры этого пользователя, исключая текущую игру, к которой присоединяемся
|
||
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
||
|
||
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
|
||
// GameInstance.addPlayer принимает socket, chosenCharacterKey (null для присоединения), identifier
|
||
if (game.addPlayer(socket, null, identifier)) { // chosenCharacterKey для присоединяющегося игрока не нужен, передаем null
|
||
this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) присоединился к PvP игре ${gameId}`);
|
||
|
||
// --- Логика старта игры ---
|
||
// Если игра PvP и теперь с 2 игроками, запускаем ее немедленно
|
||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||
game.startGame(); // Запускаем игру
|
||
} else {
|
||
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||
// initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
|
||
// Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
|
||
this._cleanupGame(gameId, 'initialization_failed');
|
||
}
|
||
|
||
// Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
|
||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||
|
||
// Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
|
||
// ownerIdentifier игры (идентификатор создателя) также остается.
|
||
|
||
this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
|
||
}
|
||
// --- КОНЕЦ Логики старта игры ---
|
||
|
||
|
||
} else {
|
||
// Сообщение об ошибке отправляется из game.addPlayer
|
||
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}.`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ищет случайную ожидающую PvP игру и присоединяет игрока к ней.
|
||
* Если подходящих игр нет, создает новую ожидающую игру.
|
||
* @param {object} socket - Сокет игрока.
|
||
* @param {string} [chosenCharacterKeyForCreation='elena'] - Выбранный персонаж, если придется создавать новую игру.
|
||
* @param {string|number} identifier - ID пользователя (userId).
|
||
*/
|
||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { // В findRandomGame всегда передается userId
|
||
// Удаляем старые ожидающие игры этого пользователя
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
|
||
// Проверяем, не находится ли пользователь уже в какой-то игре
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${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];
|
||
// Проверяем, что игра существует, PvP, в ней только 1 игрок и это НЕ игра, которую создал сам текущий пользователь
|
||
// Игрок не должен присоединяться к игре, которую создал сам.
|
||
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); // В ожидающей игре всегда 1 игрок, он и есть players[0]
|
||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
||
gameIdToJoin = id; // Нашли игру с предпочтительным оппонентом
|
||
break; // Выходим из цикла, т.к. нашли лучший вариант
|
||
}
|
||
// Если предпочтительного не нашли в этом цикле, сохраняем ID первой попавшейся (не своей) игры
|
||
if (!gameIdToJoin) gameIdToJoin = id; // Сохраняем, но продолжаем искать предпочтительную
|
||
}
|
||
}
|
||
|
||
if (gameIdToJoin) {
|
||
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) нашел игру ${gameIdToJoin} и присоединяется.`);
|
||
this.joinGame(socket, gameIdToJoin, identifier); // Используем joinGame, т.к. логика присоединения одинакова
|
||
} else {
|
||
// Если свободных игр нет, создаем новую с выбранным персонажем
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) не нашел свободных игр. Создает новую.`);
|
||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); // Используем createGame
|
||
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
|
||
// userIdentifierToGameId уже обновлен в createGame
|
||
socket.emit('noPendingGamesFound', {
|
||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||
gameId: this.userIdentifierToGameId[identifier], // ID только что созданной игры
|
||
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Перенаправляет действие игрока соответствующему экземпляру игры.
|
||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||
* @param {object} actionData - Данные о действии.
|
||
*/
|
||
handlePlayerAction(identifier, actionData) { // Теперь принимаем identifier
|
||
const gameId = this.userIdentifierToGameId[identifier]; // Находим ID игры по идентификатору пользователя
|
||
const game = this.games[gameId]; // Находим экземпляр игры
|
||
|
||
if (game && game.players) {
|
||
// Находим текущий сокет ID пользователя в списке игроков этой игры
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
const currentSocketId = playerInfo?.socket?.id;
|
||
|
||
if (playerInfo && currentSocketId) {
|
||
// Проверяем, что сокет с этим ID еще подключен.
|
||
// Это дополнительная проверка, чтобы не обрабатывать действия от "зомби"-сокетов
|
||
const actualSocket = this.io.sockets.sockets.get(currentSocketId);
|
||
|
||
if (actualSocket && actualSocket.connected) {
|
||
// Передаем действие экземпляру игры, используя ТЕКУЩИЙ Socket ID
|
||
game.processPlayerAction(currentSocketId, actionData); // processPlayerAction в GameInstance использует socketId
|
||
} else {
|
||
// Если сокет не найден или не подключен, это может быть старое действие от отключившегося сокета
|
||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его текущий сокет (${currentSocketId}) не найден или отключен.`);
|
||
// Не отправляем ошибку клиенту, так как он, вероятно, уже отключен или переподключается
|
||
// Клиент получит gameNotFound при следующем запросе состояния или gameError, если игра еще активна
|
||
}
|
||
} else {
|
||
// Игрок не найден в списке players этой игры по идентификатору
|
||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}) для игры ${gameId}, но его запись не найдена в game.players.`);
|
||
// В таком случае, возможно, состояние userIdentifierToGameId некорректно.
|
||
// Удаляем некорректную ссылку.
|
||
delete this.userIdentifierToGameId[identifier];
|
||
// Оповещаем клиента, что игра не найдена (он должен будет запросить состояние)
|
||
const playerSocket = this.io.sockets.sockets.get(identifier); // Попробуем найти сокет по идентификатору (если он был socket.id)
|
||
if (!playerSocket && playerInfo?.socket) { // Если не нашли по identifier, попробуем по сокету из playerInfo
|
||
playerSocket = playerInfo.socket;
|
||
}
|
||
if (playerSocket) {
|
||
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||
}
|
||
}
|
||
} else {
|
||
// Если игра не найдена по userIdentifierToGameId[identifier]
|
||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его игра (ID: ${gameId}) не найдена в GameManager.`);
|
||
// Удаляем некорректную ссылку
|
||
delete this.userIdentifierToGameId[identifier];
|
||
// Отправляем gameNotFound клиенту, если можем его найти (по identifier, если это socket.id)
|
||
const playerSocket = this.io.sockets.sockets.get(identifier);
|
||
if (playerSocket) {
|
||
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает отключение сокета игрока.
|
||
* Вызывается из bc.js при событии 'disconnect'.
|
||
* @param {string} socketId - ID отключившегося сокета.
|
||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||
*/
|
||
handleDisconnect(socketId, identifier) { // Принимаем и socketId, и identifier
|
||
// Ищем игру по идентификатору пользователя (более надежный способ после переподключения)
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
const game = this.games[gameId];
|
||
|
||
// Если игра найдена и в ней есть игрок с этим идентификатором (или сокетом)
|
||
if (game && game.players) {
|
||
// Находим информацию об игроке по идентификатору
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
|
||
if (playerInfo) {
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socketId}) отключился. В игре ${gameId}.`);
|
||
|
||
// Удаляем игрока из экземпляра игры, передавая Socket ID, который отключился
|
||
// GameInstance.removePlayer принимает socketId
|
||
game.removePlayer(socketId); // Передаем socketId для удаления конкретного сокета
|
||
|
||
// После удаления игрока из GameInstance, проверяем состояние игры и GameManager
|
||
if (game.playerCount === 0) {
|
||
// Если в игре больше нет игроков, удаляем ее из GameManager
|
||
console.log(`[GameManager] Игра ${gameId} пуста после дисконнекта ${socketId} (идентификатор ${identifier}). Удаляем.`);
|
||
// Используем централизованную функцию очистки
|
||
this._cleanupGame(gameId, 'player_count_zero_on_disconnect');
|
||
|
||
} else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||
// Если игра PvP, остался 1 игрок, и она еще не окончена (из-за дисконнекта),
|
||
// возвращаем ее в список ожидающих.
|
||
console.log(`[GameManager] Игра ${gameId} (PvP) теперь с 1 игроком после дисконнекта ${socketId} (идентификатор ${identifier}). Возвращаем в список ожидания.`);
|
||
if (!this.pendingPvPGames.includes(gameId)) {
|
||
this.pendingPvPGames.push(gameId);
|
||
}
|
||
// Удаляем ссылку на игру только для отключившегося идентификатора
|
||
delete this.userIdentifierToGameId[identifier];
|
||
|
||
// ownerIdentifier игры (если был userId) останется тем же, даже если отключился владелец.
|
||
// Это OK, ownerIdentifier используется для _removePreviousPendingGames.
|
||
|
||
this.broadcastAvailablePvPGames(); // Обновляем список у всех
|
||
} else if (game.gameState?.isGameOver) {
|
||
// Если игра была окончена (например, дисконнект приводил к gameOver),
|
||
// просто удаляем ссылку на игру для отключившегося идентификатора.
|
||
console.log(`[GameManager] Игрок ${identifier} отключился из завершенной игры ${gameId}. Удаляем ссылку.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
} else {
|
||
// Игра не пуста и не вернулась в ожидание (например, AI игра, где остался игрок,
|
||
// или PvP игра с 2 игроками, где один отключился, а второй остался)
|
||
// Ссылка userIdentifierToGameId[identifier] для отключившегося игрока должна быть удалена.
|
||
console.log(`[GameManager] Игрок ${identifier} отключился из активной игры ${gameId} (mode: ${game.mode}, players: ${game.playerCount}). Удаляем ссылку.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
} else {
|
||
// Игра найдена, но игрока с этим идентификатором или сокетом в game.players нет.
|
||
// Это может означать, что сокет отключился, но запись игрока была удалена раньше,
|
||
// или identifier некорректен.
|
||
console.warn(`[GameManager] Игрок с идентификатором ${identifier} (сокет: ${socketId}) не найден в game.players для игры ${gameId}.`);
|
||
// Удаляем ссылку на игру для этого идентификатора, если она есть.
|
||
delete this.userIdentifierToGameId[identifier];
|
||
// Проверяем, возможно, этот сокет был в другой игре по старой ссылке socketToGame (удалено),
|
||
// или это просто отключившийся сокет без активной игры.
|
||
}
|
||
} else {
|
||
// Если игра не найдена по userIdentifierToGameId[identifier]
|
||
console.log(`[GameManager] Отключился сокет ${socketId} (идентификатор ${identifier}). Игровая сессия по этому идентификатору не найдена.`);
|
||
// Убеждаемся, что ссылка userIdentifierToGameId[identifier] удалена
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Централизованная функция для очистки игры после ее завершения.
|
||
* Удаляет экземпляр игры и все связанные с ней ссылки.
|
||
* Вызывается из GameInstance при gameOver (по HP или дисконнекту).
|
||
* @param {string} gameId - ID завершенной игры.
|
||
* @param {string} reason - Причина завершения (для логирования).
|
||
* @returns {boolean} true, если игра найдена и очищена, иначе false.
|
||
*/
|
||
_cleanupGame(gameId, reason = 'unknown_reason') { // <-- НОВЫЙ ПРИВАТНЫЙ МЕТОД
|
||
const game = this.games[gameId];
|
||
if (!game) {
|
||
console.warn(`[GameManager] _cleanupGame called for unknown game ID: ${gameId}`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`[GameManager] Cleaning up game ${gameId} (Mode: ${game.mode}, Reason: ${reason})...`);
|
||
|
||
// Удаляем ссылку userIdentifierToGameId для всех игроков, которые были в этой игре
|
||
// Перебираем players в GameInstance, чтобы получить идентификаторы
|
||
Object.values(game.players).forEach(playerInfo => {
|
||
if (playerInfo && playerInfo.identifier && this.userIdentifierToGameId[playerInfo.identifier] === gameId) {
|
||
delete this.userIdentifierToGameId[playerInfo.identifier];
|
||
console.log(`[GameManager] Removed userIdentifierToGameId for ${playerInfo.identifier}.`);
|
||
} else if (playerInfo && playerInfo.identifier) {
|
||
console.warn(`[GameManager] User ${playerInfo.identifier} in game ${gameId} has incorrect userIdentifierToGameId reference.`);
|
||
// Если ссылка некорректна, ничего не удаляем.
|
||
}
|
||
});
|
||
|
||
// Удаляем ID игры из списка ожидающих, если она там была
|
||
const pendingIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (pendingIndex > -1) {
|
||
this.pendingPvPGames.splice(pendingIndex, 1);
|
||
console.log(`[GameManager] Removed game ${gameId} from pendingPvPGames.`);
|
||
}
|
||
|
||
// Удаляем сам экземпляр игры
|
||
delete this.games[gameId];
|
||
console.log(`[GameManager] Deleted GameInstance for game ${gameId}.`);
|
||
|
||
// Оповещаем клиентов об обновленном списке игр (может понадобиться, если удалена ожидающая игра)
|
||
// Или если активная игра была удалена, и игроки вернутся в лобби.
|
||
this.broadcastAvailablePvPGames();
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
/**
|
||
* Формирует список доступных для присоединения PvP игр для клиента.
|
||
* @returns {Array<object>} Массив объектов с информацией об играх.
|
||
*/
|
||
getAvailablePvPGamesListForClient() {
|
||
return this.pendingPvPGames
|
||
.map(gameId => {
|
||
const game = this.games[gameId];
|
||
// Проверяем, что игра существует, это PvP, в ней 1 игрок, и она не окончена
|
||
// gameState.isGameOver проверяется, чтобы исключить игры, которые могли завершиться сразу (очень маловероятно)
|
||
if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
||
let firstPlayerUsername = 'Игрок';
|
||
let firstPlayerCharacterName = '';
|
||
|
||
// Находим информацию о первом игроке (он всегда в слоте GAME_CONFIG.PLAYER_ID в ожидающей игре)
|
||
const firstPlayerInfo = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
|
||
if (firstPlayerInfo) {
|
||
// Получаем имя пользователя из userData, если залогинен
|
||
if (firstPlayerInfo.socket?.userData?.username) {
|
||
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
|
||
} else {
|
||
// Если нет userData.username, используем часть identifier
|
||
firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0,6)}`; // Приводим identifier к строке
|
||
}
|
||
|
||
// Получаем имя персонажа из chosenCharacterKey
|
||
const charKey = firstPlayerInfo.chosenCharacterKey;
|
||
if (charKey) {
|
||
// Используем _getCharacterBaseData напрямую, т.к. gameData доступен
|
||
const charBaseStats = this._getCharacterBaseData(charKey);
|
||
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}.`);
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo (Player 1) не найдена для ожидающей игры ${gameId}.`);
|
||
firstPlayerUsername = 'Неизвестный игрок'; // Если даже игрока не нашли в players
|
||
}
|
||
|
||
// Формируем строку статуса для отображения в списке
|
||
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
|
||
if (firstPlayerCharacterName) {
|
||
statusString += ` за ${firstPlayerCharacterName}`;
|
||
}
|
||
statusString += `)`;
|
||
|
||
return {
|
||
id: gameId, // Отправляем полный ID, но в списке UI показываем обрезанный
|
||
status: statusString
|
||
};
|
||
}
|
||
// Если игра не соответствует критериям ожидающей (например, пуста, заполнена, окончена), не включаем ее
|
||
if (game && !this.pendingPvPGames.includes(gameId)) {
|
||
// Если игра есть, но не в pendingPvPGames, она не должна тут обрабатываться.
|
||
} else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) {
|
||
// Игра с 1 игроком, но окончена или не инициализирована - не показывать
|
||
} else if (game && game.playerCount === 2) {
|
||
// Игра заполнена - не показывать
|
||
} else if (game && game.playerCount === 0) {
|
||
// Игра пуста - ее надо было удалить при дисконнекте последнего игрока.
|
||
// Возможно, тут нужна очистка таких "потерянных" игр.
|
||
console.warn(`[GameManager] getAvailablePvPGamesList: Найдена пустая игра ${gameId} в games. Удаляем.`);
|
||
delete this.games[gameId]; // Удаляем потерянную игру
|
||
// Очистка из pendingPvPGames не нужна, т.к. она удаляется при playerCount === 0
|
||
}
|
||
|
||
return null; // Исключаем игры, не соответствующие критериям или удаленные
|
||
})
|
||
.filter(info => info !== null); // Удаляем null из результатов map
|
||
}
|
||
|
||
/**
|
||
* Отправляет обновленный список доступных PvP игр всем подключенным клиентам.
|
||
*/
|
||
broadcastAvailablePvPGames() {
|
||
const availableGames = this.getAvailablePvPGamesListForClient();
|
||
this.io.emit('availablePvPGamesList', availableGames);
|
||
console.log(`[GameManager] Обновлен список доступных PvP игр. Всего: ${availableGames.length}`);
|
||
}
|
||
|
||
/**
|
||
* Получает список активных игр для отладки на сервере.
|
||
* @returns {Array<object>} Список объектов с краткой информацией об играх.
|
||
*/
|
||
getActiveGamesList() { // Для отладки на сервере
|
||
return Object.values(this.games).map(game => {
|
||
// Получаем имена персонажей из gameState, если игра инициализирована, иначе из chosenCharacterKey/default
|
||
let playerSlotCharName = game.gameState?.player?.name || (game.playerCharacterKey ? this._getCharacterBaseData(game.playerCharacterKey)?.name : 'N/A (ожидание)');
|
||
let opponentSlotCharName = game.gameState?.opponent?.name || (game.opponentCharacterKey ? this._getCharacterBaseData(game.opponentCharacterKey)?.name : 'N/A (ожидание)');
|
||
|
||
// Проверяем наличие игроков в слотах, чтобы уточнить статус
|
||
const playerInSlot1 = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
const playerInSlot2 = Object.values(game.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||
|
||
if (!playerInSlot1) playerSlotCharName = 'Пусто';
|
||
if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...'; // В PvP слоты могут быть пустыми
|
||
if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)'; // В AI слоте оппонента всегда AI
|
||
|
||
return {
|
||
id: game.id.substring(0,8), // Обрезанный ID для удобства
|
||
mode: game.mode,
|
||
playerCount: game.playerCount,
|
||
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A (Не инициализирована)',
|
||
playerSlot: playerSlotCharName,
|
||
opponentSlot: opponentSlotCharName,
|
||
ownerIdentifier: game.ownerIdentifier || 'N/A',
|
||
pending: this.pendingPvPGames.includes(game.id),
|
||
turn: game.gameState ? `Ход ${game.gameState.turnNumber}, ${game.gameState.isPlayerTurn ? (playerInSlot1?.identifier || 'Player Slot') : (playerInSlot2?.identifier || 'Opponent Slot')}` : 'N/A'
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает запрос клиента на gameState (например, при переподключении).
|
||
* Находит игру пользователя по его идентификатору и отправляет ему актуальное состояние.
|
||
* Также обновляет ссылку на сокет в GameInstance.
|
||
* @param {object} socket - Сокет клиента, запросившего состояние.
|
||
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||
*/
|
||
handleRequestGameState(socket, identifier) { // Принимаем socket и identifier
|
||
// Ищем игру пользователя по его идентификатору
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
let game = null;
|
||
|
||
if (gameId) {
|
||
game = this.games[gameId];
|
||
}
|
||
|
||
// Если игра найдена и она существует, и в ней есть игрок с этим идентификатором
|
||
if (game && game.players) {
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
|
||
if (playerInfo) {
|
||
// Проверяем, если игра окончена, не восстанавливаем состояние, а информируем
|
||
if (game.gameState?.isGameOver) {
|
||
console.log(`[GameManager] Reconnected user ${identifier} to game ${gameId} which is already over. Sending gameNotFound.`);
|
||
// Удаляем ссылку на оконченную игру для этого пользователя
|
||
delete this.userIdentifierToGameId[identifier];
|
||
// Отправляем gameNotFound, чтобы клиент вернулся в меню
|
||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||
return; // Прекращаем обработку
|
||
}
|
||
|
||
|
||
console.log(`[GameManager] Found game ${gameId} for identifier ${identifier} (role ${playerInfo.id}). Reconnecting socket ${socket.id}.`);
|
||
|
||
// --- Обновляем GameInstance: заменяем старый сокет на новый для этого игрока ---
|
||
// Удаляем старую запись игрока по старому socket.id, если она есть и отличается
|
||
const oldSocketId = playerInfo.socket?.id;
|
||
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
||
console.log(`[GameManager] Updating socket ID for player ${identifier} from ${oldSocketId} to ${socket.id} in game ${gameId}.`);
|
||
delete game.players[oldSocketId]; // Удаляем запись по старому socketId
|
||
// playerCount не уменьшаем/увеличиваем, т.к. это тот же игрок, просто сменил сокет
|
||
// Удаляем ссылку на старый сокет по роли
|
||
if (game.playerSockets[playerInfo.id]?.id === oldSocketId) {
|
||
delete game.playerSockets[playerInfo.id];
|
||
}
|
||
}
|
||
|
||
// Добавляем или обновляем запись для нового сокета, связывая его с существующим идентификатором игрока
|
||
game.players[socket.id] = playerInfo; // Переиспользуем существующий объект playerInfo
|
||
game.players[socket.id].socket = socket; // Обновляем объект сокета
|
||
// Ensure the identifier and role are correct on the new socket entry
|
||
game.players[socket.id].identifier = identifier; // Make sure identifier is set (уже должно быть, но на всякий случай)
|
||
// playerInfo.id should already be correct (player/opponent role)
|
||
|
||
game.playerSockets[playerInfo.id] = socket; // Обновляем ссылку на сокет по роли
|
||
|
||
// Убеждаемся, что новый socket.id теперь связан с этой игрой в GameManager - НЕ НУЖНО, socketToGame удален
|
||
// this.socketToGame[socket.id] = game.id;
|
||
|
||
|
||
// Присоединяем новый сокет к комнате Socket.IO
|
||
socket.join(game.id);
|
||
// --- КОНЕЦ Обновления сокета ---
|
||
|
||
|
||
// Получаем данные персонажей с точки зрения этого клиента
|
||
// playerInfo.chosenCharacterKey - это персонаж этого клиента
|
||
const playerCharDataForClient = this._getCharacterData(playerInfo.chosenCharacterKey);
|
||
// Определяем ключ персонажа оппонента с точки зрения этого клиента
|
||
const opponentActualSlotId = playerInfo.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null; // Берем из gameState, т.к. там актуальное состояние слотов
|
||
// Если оппонент еще не определен в gameState (PvP ожидание), используем playerCharacterKey/opponentCharacterKey из gameInstance
|
||
// ВАЖНО: при переподключении к *активной* игре, gameState.opponent.characterKey ДОЛЖЕН БЫТЬ определен.
|
||
// Если он null, это может быть PvP ожидание или некорректное состояние.
|
||
if (!opponentCharacterKeyForClient) {
|
||
// Попробуем найти ключ из GameInstance properties (они устанавливаются при инициализации)
|
||
const opponentSlotKeyInInstance = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.playerCharacterKey : game.opponentCharacterKey; // ИСПРАВЛЕНО: Логика получения ключа оппонента
|
||
opponentCharacterKeyForClient = opponentSlotKeyInInstance;
|
||
// Если даже из GameInstance properties ключ null, это точно PvP ожидание или критическая ошибка
|
||
}
|
||
const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient); // Данные оппонента с т.з. клиента
|
||
|
||
|
||
if (playerCharDataForClient && opponentCharDataForClient && game.gameState) {
|
||
// Проверяем, готово ли gameState к игре (определены оба бойца)
|
||
const isGameReadyForPlay = (game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2);
|
||
const isOpponentDefinedInState = game.gameState.opponent?.characterKey && game.gameState.opponent?.name !== 'Ожидание игрока...';
|
||
|
||
|
||
socket.emit('gameState', {
|
||
gameId: game.id,
|
||
yourPlayerId: playerInfo.id, // ID слота этого клиента в игре
|
||
gameState: game.gameState,
|
||
playerBaseStats: playerCharDataForClient.baseStats, // Статы "моего" персонажа для клиента
|
||
opponentBaseStats: opponentCharDataForClient.baseStats, // Статы "моего" оппонента для клиента
|
||
playerAbilities: playerCharDataForClient.abilities, // Абилки "моего" персонажа для клиента
|
||
opponentAbilities: opponentCharDataForClient.abilities, // Абилки "моего" оппонента для клиента
|
||
log: game.consumeLogBuffer(), // Отправляем текущий лог и очищаем буфер игры
|
||
clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига
|
||
});
|
||
console.log(`[GameManager] Sent gameState to socket ${socket.id} (identifier: ${identifier}) for game ${game.id}.`);
|
||
|
||
// Логика старта игры при переподключении (если она еще не началась)
|
||
// Эта логика должна быть только для случая, когда переподключившийся игрок ЗАВЕРШАЕТ состав игры
|
||
// (например, второй игрок в PvP переподключился к ожидающей игре).
|
||
// Если игра уже началась, startGame не должен вызываться повторно.
|
||
// Проверяем: игра не окончена, готова к игре (2 игрока или AI), и состояние оппонента НЕ БЫЛО определено до этого запроса (признак не полностью стартовавшей игры)
|
||
if (!game.gameState.isGameOver && isGameReadyForPlay && !isOpponentDefinedInState) {
|
||
console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect (Opponent state missing). Initializing/Starting.`);
|
||
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||
const isInitialized = game.initializeGame(); // Переинициализируем state полностью с обоими персонажами
|
||
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||
game.startGame(); // Запускаем игру (это отправит gameStarted всем, включая этого клиента)
|
||
} else {
|
||
console.error(`[GameManager] Failed to initialize game ${game.id} on reconnect. Cannot start.`);
|
||
// Дополнительная обработка ошибки, возможно, уведомить игроков
|
||
this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения. Не удалось инициализировать игру.' });
|
||
// Если инициализация провалилась, игра в некорректном состоянии, нужно ее удалить
|
||
this._cleanupGame(gameId, 'reconnect_initialization_failed');
|
||
}
|
||
}
|
||
// Если игра уже активно идет (не окончена, не ожидание) и состояние оппонента БЫЛО определено,
|
||
// то startGame не вызывается повторно. Клиент получит gameStateUpdate от обычного хода игры.
|
||
// Если игра PvP ожидающая (1 игрок), startGame не вызывается, isGameReadyForPlay будет false.
|
||
else if (!isGameReadyForPlay) {
|
||
console.log(`[GameManager] Reconnected user ${identifier} to pending game ${gameId}. Sending gameState and waiting status.`);
|
||
// Если это ожидающая игра, убедимся, что клиент получает статус ожидания
|
||
socket.emit('waitingForOpponent');
|
||
} else if (game.gameState.isGameOver) {
|
||
console.log(`[GameManager] Reconnected to game ${gameId} which is already over. Sending gameNotFound.`);
|
||
// Если игра окончена, client.js должен по gameState.isGameOver показать модалку.
|
||
// Но чтобы гарантировать возврат в меню при последующих запросах, лучше отправить gameNotFound.
|
||
// Удаляем ссылку на оконченную игру для этого пользователя
|
||
delete this.userIdentifierToGameId[identifier];
|
||
// Отправляем gameNotFound
|
||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||
} else {
|
||
// Переподключение к активной игре, которая уже полностью стартовала.
|
||
console.log(`[GameManager] Reconnected user ${identifier} to active game ${gameId}. gameState sent.`);
|
||
}
|
||
|
||
|
||
} else {
|
||
console.error(`[GameManager] Failed to send gameState to ${socket.id} (identifier ${identifier}) for game ${gameId}: missing character data or gameState.`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||
// Если данные для отправки некорректны, игра в некорректном состоянии, нужно ее удалить
|
||
this._cleanupGame(gameId, 'reconnect_send_failed');
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия в некорректном состоянии и была завершена.' });
|
||
}
|
||
|
||
} else {
|
||
// Игра найдена по идентификатору пользователя, но игрока с этим идентификатором нет в players этой игры.
|
||
// Это очень странная ситуация, возможно, state userIdentifierToGameId некорректен.
|
||
console.warn(`[GameManager] Found game ${gameId} by identifier ${identifier}, but player with this identifier not found in game.players.`);
|
||
// Удаляем некорректную ссылку и отправляем gameNotFound
|
||
delete this.userIdentifierToGameId[identifier];
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена. Возможно, идентификатор пользователя некорректен.' });
|
||
}
|
||
} else {
|
||
// Игра не найдена по userIdentifierToGameId[identifier]
|
||
console.log(`[GameManager] No active or pending game found for identifier ${identifier}.`);
|
||
socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' }); // Уведомляем клиента, что игра не найдена
|
||
}
|
||
}
|
||
|
||
// --- Вспомогательные функции для получения данных персонажа из data.js ---
|
||
// Скопировано из gameInstance.js, т.к. gameManager тоже использует gameData напрямую
|
||
/**
|
||
* Получает базовые статы и список способностей для персонажа по ключу.
|
||
* Эти функции предназначены для использования ВНУТРИ GameManager или GameInstance.
|
||
* @param {string} key - Ключ персонажа ('elena', 'balard', 'almagest').
|
||
* @returns {{baseStats: object, abilities: array}|null} Объект с базовыми статами и способностями, или null.
|
||
*/
|
||
_getCharacterData(key) {
|
||
if (!key) { console.warn("GameManager::_getCharacterData called with null/undefined key."); return null; }
|
||
switch (key) {
|
||
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
|
||
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; // Балард использует opponentAbilities из data.js
|
||
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; // Альмагест использует almagestAbilities из data.js
|
||
default: console.error(`GameManager::_getCharacterData: Unknown character key "${key}"`); return null;
|
||
}
|
||
}
|
||
/**
|
||
* Получает только базовые статы для персонажа по ключу.
|
||
* @param {string} key - Ключ персонажа.
|
||
* @returns {object|null} Базовые статы или null.
|
||
*/
|
||
_getCharacterBaseData(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.baseStats : null;
|
||
}
|
||
/**
|
||
* Получает только список способностей для персонажа по ключу.
|
||
* @param {string} key - Ключ персонажа.
|
||
* @returns {array|null} Список способностей или null.
|
||
*/
|
||
_getCharacterAbilities(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.abilities : null;
|
||
}
|
||
}
|
||
|
||
module.exports = GameManager; |