559 lines
40 KiB
JavaScript
559 lines
40 KiB
JavaScript
// /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 = {}; // { gameId: GameInstance }
|
||
this.userIdentifierToGameId = {}; // { userId: gameId }
|
||
this.pendingPvPGames = []; // Массив gameId ожидающих PvP игр
|
||
console.log("[GameManager] Инициализирован.");
|
||
}
|
||
|
||
_cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') {
|
||
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||
if (oldPendingGameId && this.games[oldPendingGameId]) {
|
||
const gameToRemove = this.games[oldPendingGameId];
|
||
// Убеждаемся, что это именно ожидающая PvP игра этого пользователя
|
||
if (gameToRemove.mode === 'pvp' &&
|
||
gameToRemove.ownerIdentifier === identifier && // Он владелец
|
||
gameToRemove.playerCount === 1 && // В игре только он
|
||
this.pendingPvPGames.includes(oldPendingGameId) && // Игра в списке ожидающих
|
||
(!gameToRemove.gameState || !gameToRemove.gameState.isGameOver) // И она не завершена
|
||
) {
|
||
console.log(`[GameManager._cleanupPreviousPendingGameForUser] Пользователь ${identifier} имеет существующую ожидающую PvP игру ${oldPendingGameId}. Удаление. Причина: ${reasonSuffix}`);
|
||
this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_pvp_game_${reasonSuffix}`);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
|
||
console.log(`[GameManager.createGame] Пользователь: ${identifier} (Socket: ${socket.id}), Режим: ${mode}, Персонаж: ${chosenCharacterKey || 'По умолчанию'}`);
|
||
|
||
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
|
||
|
||
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
|
||
const existingGame = this.games[existingGameIdForUser];
|
||
if (existingGame.gameState && existingGame.gameState.isGameOver) {
|
||
console.warn(`[GameManager.createGame] Пользователь ${identifier} был в завершенной игре ${existingGameIdForUser}. Очистка перед созданием новой.`);
|
||
this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`);
|
||
} else {
|
||
const isHisOwnPendingPvp = existingGame.mode === 'pvp' &&
|
||
existingGame.ownerIdentifier === identifier &&
|
||
existingGame.playerCount === 1 &&
|
||
this.pendingPvPGames.includes(existingGameIdForUser);
|
||
|
||
if (!isHisOwnPendingPvp) {
|
||
console.warn(`[GameManager.createGame] Пользователь ${identifier} уже в активной игре ${existingGameIdForUser} (режим: ${existingGame.mode}, владелец: ${existingGame.ownerIdentifier}). Невозможно создать новую.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`);
|
||
console.log(`[GameManager.createGame] После возможной очистки, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`);
|
||
|
||
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
|
||
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
|
||
console.error(`[GameManager.createGame] КРИТИЧЕСКАЯ ОШИБКА ЛОГИКИ: Пользователь ${identifier} все еще сопоставлен с активной игрой ${stillExistingGameIdAfterCleanup} после попытки очистки. Создание отклонено.`);
|
||
socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
|
||
const gameId = uuidv4();
|
||
console.log(`[GameManager.createGame] Новый GameID: ${gameId}`);
|
||
const game = new GameInstance(gameId, this.io, mode, this);
|
||
this.games[gameId] = game;
|
||
|
||
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
|
||
|
||
if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameId;
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
const assignedPlayerId = playerInfo?.id;
|
||
const actualCharacterKey = playerInfo?.chosenCharacterKey;
|
||
|
||
if (!assignedPlayerId || !actualCharacterKey) {
|
||
console.error(`[GameManager.createGame] КРИТИЧЕСКИ: Не удалось получить роль/ключ персонажа после addPlayer для ${identifier} в игре ${gameId}. Очистка.`);
|
||
this._cleanupGame(gameId, 'player_info_missing_after_add_on_create');
|
||
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
|
||
return;
|
||
}
|
||
|
||
console.log(`[GameManager.createGame] Игрок ${identifier} добавлен в игру ${gameId} как ${assignedPlayerId}. Карта пользователя обновлена. Текущая карта для ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
||
socket.emit('gameCreated', {
|
||
gameId: gameId,
|
||
mode: mode,
|
||
yourPlayerId: assignedPlayerId,
|
||
chosenCharacterKey: actualCharacterKey
|
||
});
|
||
|
||
if (mode === 'ai') {
|
||
if (game.initializeGame()) {
|
||
console.log(`[GameManager.createGame] AI игра ${gameId} инициализирована GameManager, запуск...`);
|
||
game.startGame();
|
||
} else {
|
||
console.error(`[GameManager.createGame] Инициализация AI игры ${gameId} не удалась в GameManager. Очистка.`);
|
||
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
|
||
}
|
||
} else if (mode === 'pvp') {
|
||
if (game.initializeGame()) {
|
||
if (!this.pendingPvPGames.includes(gameId)) {
|
||
this.pendingPvPGames.push(gameId);
|
||
}
|
||
socket.emit('waitingForOpponent');
|
||
this.broadcastAvailablePvPGames();
|
||
} else {
|
||
console.error(`[GameManager.createGame] Инициализация PvP игры ${gameId} (один игрок) не удалась. Очистка.`);
|
||
this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player');
|
||
}
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.createGame] game.addPlayer не удалось для ${identifier} в ${gameId}. Очистка.`);
|
||
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create');
|
||
}
|
||
}
|
||
|
||
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
|
||
console.log(`[GameManager.joinGame] Пользователь: ${identifier} (Socket: ${socket.id}) пытается присоединиться к ${gameIdToJoin} с персонажем ${chosenCharacterKey || 'По умолчанию'}`);
|
||
const gameToJoin = this.games[gameIdToJoin];
|
||
|
||
if (!gameToJoin) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||
if (gameToJoin.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_game_${identifier}`); return; }
|
||
if (gameToJoin.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; }
|
||
|
||
const playerInfoInTargetGame = Object.values(gameToJoin.players).find(p => p.identifier === identifier);
|
||
if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
|
||
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||
}
|
||
// Запрещаем владельцу "присоединяться" к своей ожидающей игре как новый игрок, если он не был временно отключен.
|
||
// Если он хочет вернуться, он должен использовать requestGameState.
|
||
if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
|
||
console.warn(`[GameManager.joinGame] Пользователь ${identifier} пытается присоединиться к своей игре ${gameIdToJoin}, где он владелец и не отключен. Обработка как запрос на переподключение.`);
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
|
||
const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier];
|
||
if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) {
|
||
console.warn(`[GameManager.joinGame] Пользователь ${identifier} был в завершенной игре ${currentActiveGameIdUserIsIn} при попытке присоединиться к ${gameIdToJoin}. Очистка старой.`);
|
||
this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`);
|
||
}
|
||
|
||
const stillExistingGameIdForUser = this.userIdentifierToGameId[identifier];
|
||
if (stillExistingGameIdForUser && stillExistingGameIdForUser !== gameIdToJoin && this.games[stillExistingGameIdForUser] && !this.games[stillExistingGameIdForUser].gameState?.isGameOver) {
|
||
const usersCurrentGame = this.games[stillExistingGameIdForUser];
|
||
const isHisOwnPendingPvp = usersCurrentGame.mode === 'pvp' &&
|
||
usersCurrentGame.ownerIdentifier === identifier &&
|
||
usersCurrentGame.playerCount === 1 &&
|
||
this.pendingPvPGames.includes(stillExistingGameIdForUser);
|
||
|
||
if (isHisOwnPendingPvp) {
|
||
console.log(`[GameManager.joinGame] Пользователь ${identifier} является владельцем ожидающей игры ${stillExistingGameIdForUser}, но хочет присоединиться к ${gameIdToJoin}. Очистка старой игры.`);
|
||
this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`);
|
||
} else {
|
||
console.warn(`[GameManager.joinGame] Пользователь ${identifier} находится в другой активной игре ${stillExistingGameIdForUser}. Невозможно присоединиться к ${gameIdToJoin}.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
}
|
||
console.log(`[GameManager.joinGame] После возможной очистки перед присоединением, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`);
|
||
|
||
const charKeyForJoin = chosenCharacterKey || 'elena';
|
||
if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||
const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier);
|
||
|
||
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
|
||
console.error(`[GameManager.joinGame] КРИТИЧЕСКИ: Не удалось получить роль/ключ персонажа после addPlayer для ${identifier}, присоединяющегося к ${gameIdToJoin}.`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
|
||
if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier];
|
||
return;
|
||
}
|
||
console.log(`[GameManager.joinGame] Игрок ${identifier} добавлен/переподключен к ${gameIdToJoin} как ${joinedPlayerInfo.id}. Карта пользователя обновлена. Текущая карта для ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
||
socket.emit('gameCreated', {
|
||
gameId: gameIdToJoin,
|
||
mode: gameToJoin.mode,
|
||
yourPlayerId: joinedPlayerInfo.id,
|
||
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
|
||
});
|
||
|
||
if (gameToJoin.playerCount === 2) {
|
||
console.log(`[GameManager.joinGame] Игра ${gameIdToJoin} теперь заполнена. Инициализация и запуск.`);
|
||
// Важно! Инициализация может обновить ключи персонажей, если они были одинаковыми.
|
||
if (gameToJoin.initializeGame()) {
|
||
gameToJoin.startGame();
|
||
} else {
|
||
this._cleanupGame(gameIdToJoin, 'full_init_fail_pvp_join_gm'); return;
|
||
}
|
||
const idx = this.pendingPvPGames.indexOf(gameIdToJoin);
|
||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer вернул false для пользователя ${identifier} в игре ${gameIdToJoin}.`);
|
||
}
|
||
}
|
||
|
||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||
console.log(`[GameManager.findRandomPvPGame] Пользователь: ${identifier} (Socket: ${socket.id}), Персонаж для создания: ${chosenCharacterKeyForCreation}`);
|
||
|
||
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
|
||
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
|
||
const existingGame = this.games[existingGameIdForUser];
|
||
if (existingGame.gameState && existingGame.gameState.isGameOver) {
|
||
console.warn(`[GameManager.findRandomPvPGame] Пользователь ${identifier} был в завершенной игре ${existingGameIdForUser}. Очистка.`);
|
||
this._cleanupGame(existingGameIdForUser, `stale_finished_on_find_random_${identifier}`);
|
||
} else {
|
||
console.warn(`[GameManager.findRandomPvPGame] Пользователь ${identifier} уже в активной/ожидающей игре ${existingGameIdForUser}. Невозможно найти случайную.`);
|
||
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
|
||
this.handleRequestGameState(socket, identifier); return;
|
||
}
|
||
}
|
||
|
||
this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`);
|
||
console.log(`[GameManager.findRandomPvPGame] После возможной очистки, пользователь ${identifier} сопоставлен с: ${this.userIdentifierToGameId[identifier]}`);
|
||
|
||
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
|
||
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
|
||
console.error(`[GameManager.findRandomPvPGame] КРИТИЧЕСКАЯ ОШИБКА ЛОГИКИ: Пользователь ${identifier} все еще сопоставлен с активной игрой ${stillExistingGameIdAfterCleanup} после попытки очистки. Поиск случайной игры отклонен.`);
|
||
socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию для поиска.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
|
||
let gameIdToJoin = null;
|
||
for (const id of [...this.pendingPvPGames]) {
|
||
const pendingGame = this.games[id];
|
||
if (pendingGame && pendingGame.mode === 'pvp' &&
|
||
pendingGame.playerCount === 1 &&
|
||
pendingGame.ownerIdentifier !== identifier &&
|
||
(!pendingGame.gameState || !pendingGame.gameState.isGameOver)) {
|
||
gameIdToJoin = id; break;
|
||
} else if (!pendingGame || (pendingGame?.gameState && pendingGame.gameState.isGameOver)) {
|
||
console.warn(`[GameManager.findRandomPvPGame] Найдена устаревшая/завершенная ожидающая игра ${id}. Очистка.`);
|
||
this._cleanupGame(id, `stale_finished_pending_on_find_random`);
|
||
}
|
||
}
|
||
|
||
if (gameIdToJoin) {
|
||
console.log(`[GameManager.findRandomPvPGame] Найдена ожидающая игра ${gameIdToJoin} для ${identifier}. Присоединение...`);
|
||
const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)];
|
||
this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey);
|
||
} else {
|
||
console.log(`[GameManager.findRandomPvPGame] Подходящая ожидающая игра не найдена. Создание новой PvP игры для ${identifier}.`);
|
||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||
}
|
||
}
|
||
|
||
handlePlayerAction(identifier, actionData) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
const game = this.games[gameId];
|
||
if (game) {
|
||
if (game.gameState?.isGameOver) {
|
||
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket;
|
||
if (playerSocket) {
|
||
console.warn(`[GameManager.handlePlayerAction] Действие от ${identifier} для игры ${gameId}, но игра завершена. Запрос состояния.`);
|
||
this.handleRequestGameState(playerSocket, identifier);
|
||
} else {
|
||
console.warn(`[GameManager.handlePlayerAction] Действие от ${identifier} для игры ${gameId}, игра завершена, но сокет для пользователя не найден.`);
|
||
this._cleanupGame(gameId, `action_on_over_no_socket_gm_${identifier}`);
|
||
}
|
||
return;
|
||
}
|
||
game.processPlayerAction(identifier, actionData);
|
||
} else {
|
||
console.warn(`[GameManager.handlePlayerAction] Игра для пользователя ${identifier} не найдена (сопоставлена с игрой ${gameId}). Очистка записи в карте.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена при совершении действия.' });
|
||
}
|
||
}
|
||
|
||
handlePlayerSurrender(identifier) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handlePlayerSurrender] Пользователь: ${identifier} сдался. GameID из карты: ${gameId}`);
|
||
const game = this.games[gameId];
|
||
if (game) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.handlePlayerSurrender] Пользователь ${identifier} в игре ${gameId} сдается, но игра УЖЕ ЗАВЕРШЕНА.`);
|
||
return;
|
||
}
|
||
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
|
||
else { console.error(`[GameManager.handlePlayerSurrender] КРИТИЧЕСКИ: GameInstance ${gameId} отсутствует playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); }
|
||
} else {
|
||
console.warn(`[GameManager.handlePlayerSurrender] Игра для пользователя ${identifier} не найдена. Очистка записи в карте.`);
|
||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
|
||
handleLeaveAiGame(identifier) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handleLeaveAiGame] Пользователь: ${identifier} покидает AI игру. GameID из карты: ${gameId}`);
|
||
const game = this.games[gameId];
|
||
if (game) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.warn(`[GameManager.handleLeaveAiGame] Пользователь ${identifier} в игре ${gameId} выходит, но игра УЖЕ ЗАВЕРШЕНА.`);
|
||
return;
|
||
}
|
||
if (game.mode === 'ai') {
|
||
if (typeof game.playerExplicitlyLeftAiGame === 'function') {
|
||
game.playerExplicitlyLeftAiGame(identifier);
|
||
} else {
|
||
console.error(`[GameManager.handleLeaveAiGame] КРИТИЧЕСКИ: GameInstance ${gameId} отсутствует playerExplicitlyLeftAiGame! Прямая очистка.`);
|
||
this._cleanupGame(gameId, "leave_ai_missing_method_gm");
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.handleLeaveAiGame] Пользователь ${identifier} отправил leaveAiGame, но игра ${gameId} не в режиме AI (${game.mode}).`);
|
||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||
if(clientSocket) clientSocket.emit('gameError', { message: 'Вы не в AI игре.' });
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.handleLeaveAiGame] Игра для пользователя ${identifier} не найдена. Очистка записи в карте.`);
|
||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||
if(clientSocket) clientSocket.emit('gameNotFound', { message: 'AI игра не найдена для выхода.' });
|
||
}
|
||
}
|
||
|
||
_findClientSocketByIdentifier(identifier) {
|
||
for (const s of this.io.sockets.sockets.values()) {
|
||
if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
handleDisconnect(socketId, identifier) {
|
||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, Пользователь: ${identifier}, GameID из карты: ${gameIdFromMap}`);
|
||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||
|
||
if (game) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.log(`[GameManager.handleDisconnect] Игра ${gameIdFromMap} для пользователя ${identifier} (сокет ${socketId}) УЖЕ ЗАВЕРШЕНА. Игра будет очищена своей собственной логикой или следующим релевантным действием.`);
|
||
return;
|
||
}
|
||
|
||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||
|
||
if (playerInfoInGame) { // Игрок существует в этой игре
|
||
console.log(`[GameManager.handleDisconnect] Отключающийся сокет ${socketId} для пользователя ${identifier} (Роль: ${playerInfoInGame.id}) в игре ${gameIdFromMap}. Уведомление GameInstance.`);
|
||
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
|
||
// Передаем фактический socketId, который отключился. PCH определит, устарел ли он.
|
||
game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey, socketId);
|
||
} else {
|
||
console.error(`[GameManager.handleDisconnect] КРИТИЧЕСКИ: GameInstance ${gameIdFromMap} отсутствует handlePlayerPotentiallyLeft!`);
|
||
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm");
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager.handleDisconnect] Пользователь ${identifier} сопоставлен с игрой ${gameIdFromMap}, но не найден в game.players. Это может указывать на устаревшую запись userIdentifierToGameId. Очистка карты для этого пользователя.`);
|
||
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
} else {
|
||
if (this.userIdentifierToGameId[identifier]) {
|
||
console.warn(`[GameManager.handleDisconnect] Экземпляр игры для gameId ${gameIdFromMap} (пользователь ${identifier}) не найден. Очистка устаревшей записи в карте.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
|
||
_cleanupGame(gameId, reason = 'unknown') {
|
||
console.log(`[GameManager._cleanupGame] Попытка очистки для GameID: ${gameId}, Причина: ${reason}`);
|
||
const game = this.games[gameId];
|
||
|
||
if (!game) {
|
||
console.warn(`[GameManager._cleanupGame] Экземпляр игры для ${gameId} не найден в this.games. Очистка связанных записей.`);
|
||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||
if (pendingIdx > -1) {
|
||
this.pendingPvPGames.splice(pendingIdx, 1);
|
||
console.log(`[GameManager._cleanupGame] ${gameId} удален из pendingPvPGames.`);
|
||
}
|
||
Object.keys(this.userIdentifierToGameId).forEach(idKey => {
|
||
if (this.userIdentifierToGameId[idKey] === gameId) {
|
||
delete this.userIdentifierToGameId[idKey];
|
||
console.log(`[GameManager._cleanupGame] Удалено сопоставление для пользователя ${idKey} с игрой ${gameId}.`);
|
||
}
|
||
});
|
||
this.broadcastAvailablePvPGames();
|
||
return false;
|
||
}
|
||
|
||
console.log(`[GameManager._cleanupGame] Очистка игры ${game.id}. Владелец: ${game.ownerIdentifier}. Причина: ${reason}. Игроков в игре: ${game.playerCount}`);
|
||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
|
||
|
||
if (game.gameState && !game.gameState.isGameOver) {
|
||
console.log(`[GameManager._cleanupGame] Пометка игры ${game.id} как завершенной, так как она очищается во время активности.`);
|
||
game.gameState.isGameOver = true;
|
||
// game.io.to(game.id).emit('gameOver', { winnerId: null, reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() });
|
||
}
|
||
|
||
Object.values(game.players).forEach(pInfo => {
|
||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
||
delete this.userIdentifierToGameId[pInfo.identifier];
|
||
console.log(`[GameManager._cleanupGame] Очищено userIdentifierToGameId для игрока ${pInfo.identifier}.`);
|
||
}
|
||
});
|
||
// Дополнительная проверка для владельца, если он не был в списке игроков (маловероятно, но для полноты)
|
||
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
|
||
if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) {
|
||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
||
console.log(`[GameManager._cleanupGame] Очищено userIdentifierToGameId для владельца ${game.ownerIdentifier} (не был в списке игроков).`);
|
||
}
|
||
}
|
||
|
||
|
||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||
if (pendingIdx > -1) {
|
||
this.pendingPvPGames.splice(pendingIdx, 1);
|
||
console.log(`[GameManager._cleanupGame] ${gameId} удален из pendingPvPGames.`);
|
||
}
|
||
|
||
delete this.games[gameId];
|
||
console.log(`[GameManager._cleanupGame] Экземпляр игры ${gameId} удален. Осталось игр: ${Object.keys(this.games).length}. Ожидающих: ${this.pendingPvPGames.length}. Размер карты пользователей: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||
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 p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||
let p1Username = 'Игрок';
|
||
let p1CharName = 'Неизвестный';
|
||
const ownerId = game.ownerIdentifier;
|
||
|
||
if (p1Entry) { // Используем данные из p1Entry, если он есть (более надежно)
|
||
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){ // Резервный вариант, если p1Entry почему-то нет
|
||
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
||
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
||
const ownerCharKey = game.playerCharacterKey;
|
||
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
||
}
|
||
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
||
} else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) {
|
||
console.warn(`[GameManager.getAvailablePvPGamesListForClient] Игра ${gameId} находится в pendingPvPGames, но не является допустимой ожидающей игрой (игроков: ${game.playerCount}, завершена: ${game.gameState?.isGameOver}). Удаление.`);
|
||
this._cleanupGame(gameId, 'invalid_pending_game_in_list');
|
||
}
|
||
return null;
|
||
})
|
||
.filter(info => info !== null);
|
||
}
|
||
|
||
broadcastAvailablePvPGames() {
|
||
const list = this.getAvailablePvPGamesListForClient();
|
||
this.io.emit('availablePvPGamesList', list);
|
||
}
|
||
|
||
handleRequestGameState(socket, identifier) {
|
||
const gameIdFromMap = this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager.handleRequestGameState] Пользователь: ${identifier} (Socket: ${socket.id}) запрашивает состояние. GameID из карты: ${gameIdFromMap}`);
|
||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||
|
||
if (game) {
|
||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||
|
||
if (playerInfoInGame) {
|
||
if (game.gameState?.isGameOver) {
|
||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
||
// Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это, если игра еще в this.games
|
||
return;
|
||
}
|
||
if (typeof game.handlePlayerReconnected === 'function') {
|
||
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
|
||
if (!reconnected) {
|
||
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected для ${identifier} в ${game.id} вернул false.`);
|
||
// GameInstance должен был отправить ошибку.
|
||
}
|
||
} else {
|
||
console.error(`[GameManager.handleRequestGameState] КРИТИЧЕСКИ: GameInstance ${game.id} отсутствует handlePlayerReconnected!`);
|
||
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request');
|
||
}
|
||
} else {
|
||
// Игрок сопоставлен с игрой, но НЕ НАЙДЕН в game.players. Это может произойти, если PCH еще не добавил игрока (например, F5 на экране создания игры).
|
||
// Попытаемся добавить игрока в игру, если это PvP и есть место, или если это его же игра в режиме AI.
|
||
console.warn(`[GameManager.handleRequestGameState] Пользователь ${identifier} сопоставлен с игрой ${gameIdFromMap}, но НЕ НАЙДЕН в game.players. Попытка добавить/переподключить.`);
|
||
if (game.mode === 'pvp') {
|
||
// Пытаемся присоединить, предполагая, что он мог быть удален или это F5 перед полным присоединением
|
||
const chosenCharKey = socket.handshake.query.chosenCharacterKey || 'elena'; // Получаем ключ из запроса или дефолтный
|
||
if (game.addPlayer(socket, chosenCharKey, identifier)) {
|
||
// Успешно добавили или переподключили через addPlayer -> handlePlayerReconnected
|
||
const newPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
socket.emit('gameCreated', { // Отправляем событие, как при обычном присоединении
|
||
gameId: game.id,
|
||
mode: game.mode,
|
||
yourPlayerId: newPlayerInfo.id,
|
||
chosenCharacterKey: newPlayerInfo.chosenCharacterKey
|
||
});
|
||
if (game.playerCount === 2) { // Если игра стала полной
|
||
if(game.initializeGame()) game.startGame(); else this._cleanupGame(game.id, 'init_fail_pvp_readd_gm');
|
||
const idx = this.pendingPvPGames.indexOf(game.id);
|
||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
// Не удалось добавить/переподключить через addPlayer
|
||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_readd_failed_in_gi_on_request');
|
||
}
|
||
|
||
} else if (game.mode === 'ai' && game.ownerIdentifier === identifier) {
|
||
// Для AI игры, если это владелец, пытаемся через handlePlayerReconnected
|
||
if (typeof game.handlePlayerReconnected === 'function') {
|
||
// Предполагаем, что роль PLAYER_ID, так как это AI игра и он владелец
|
||
const reconnected = game.handlePlayerReconnected(GAME_CONFIG.PLAYER_ID, socket);
|
||
if (!reconnected) {
|
||
this._handleGameRecoveryError(socket, game.id, identifier, 'ai_owner_reconnect_failed_on_request');
|
||
}
|
||
} else {
|
||
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_ai_owner_on_request');
|
||
}
|
||
} else {
|
||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_unhandled_case_on_request');
|
||
}
|
||
}
|
||
} else {
|
||
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
||
if (this.userIdentifierToGameId[identifier]) {
|
||
console.warn(`[GameManager.handleRequestGameState] Экземпляр игры для gameId ${gameIdFromMap} (пользователь ${identifier}) не найден. Очистка устаревшей записи в карте.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
|
||
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
||
console.error(`[GameManager._handleGameRecoveryError] Ошибка восстановления игры (ID: ${gameId || 'N/A'}) для пользователя ${identifier}. Причина: ${reasonCode}.`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' });
|
||
|
||
if (gameId && this.games[gameId]) {
|
||
this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`);
|
||
} else if (this.userIdentifierToGameId[identifier]) {
|
||
const problematicGameIdForUser = this.userIdentifierToGameId[identifier];
|
||
delete this.userIdentifierToGameId[identifier];
|
||
console.log(`[GameManager._handleGameRecoveryError] Очищено устаревшее userIdentifierToGameId[${identifier}], указывающее на ${problematicGameIdForUser}.`);
|
||
}
|
||
if (this.userIdentifierToGameId[identifier]) { // Финальная проверка
|
||
delete this.userIdentifierToGameId[identifier];
|
||
console.warn(`[GameManager._handleGameRecoveryError] Принудительно очищено userIdentifierToGameId[${identifier}] в качестве финальной меры.`);
|
||
}
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' });
|
||
}
|
||
}
|
||
|
||
module.exports = GameManager; |