bc/server/game/GameManager.js

559 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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;