bc/server/game/GameManager.js

562 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] Initialized.");
}
// Модифицированная функция: теперь она просто удаляет ожидающую игру пользователя, если находит.
// excludeGameId здесь больше не так критичен, так как логика вызова изменится.
_cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') {
const oldPendingGameId = this.userIdentifierToGameId[identifier];
if (oldPendingGameId && this.games[oldPendingGameId]) {
const gameToRemove = this.games[oldPendingGameId];
if (gameToRemove.mode === 'pvp' &&
gameToRemove.playerCount === 1 && // Убеждаемся, что это именно ожидающая игра с одним игроком
gameToRemove.ownerIdentifier === identifier && // И этот игрок - владелец
this.pendingPvPGames.includes(oldPendingGameId)) {
console.log(`[GameManager._cleanupPreviousPendingGameForUser] User ${identifier} performing new action. Removing previous pending PvP game: ${oldPendingGameId}. Reason: ${reasonSuffix}`);
this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_game_${reasonSuffix}`);
// _cleanupGame должен удалить запись из userIdentifierToGameId
return true; // Успешно очистили
}
}
return false; // Нечего было очищать или условия не совпали
}
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
// 1. Проверить, не находится ли пользователь уже в ЗАВЕРШЕННОЙ, но не очищенной игре.
const existingGameId = this.userIdentifierToGameId[identifier];
if (existingGameId && this.games[existingGameId]) {
const existingGame = this.games[existingGameId];
if (existingGame.gameState && existingGame.gameState.isGameOver) {
console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameId}. Cleaning it up.`);
this._cleanupGame(existingGameId, `stale_finished_on_create_${identifier}`);
// existingGameId в userIdentifierToGameId[identifier] должен был удалиться
} else if (existingGame.mode === mode && // Если это та же самая игра, к которой он пытается "пересоздать"
((mode === 'ai' && existingGame.ownerIdentifier === identifier) ||
(mode === 'pvp' && existingGame.ownerIdentifier === identifier && existingGame.playerCount === 1) ))
{
console.warn(`[GameManager.createGame] User ${identifier} trying to recreate an existing identical game ${existingGameId}. Sending current state.`);
socket.emit('gameError', { message: mode === 'pvp' ? 'Вы уже создали PvP игру и ожидаете оппонента.' : 'Вы уже в игре с AI.' });
this.handleRequestGameState(socket, identifier);
return;
} else if (existingGame.mode !== mode || existingGame.ownerIdentifier !== identifier) {
// Если он в другой активной игре (не своей ожидающей)
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
this.handleRequestGameState(socket, identifier);
return;
}
// Если это его собственная ожидающая PvP игра, мы ее удалим ниже.
}
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру.
this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`);
// 3. Если после очистки пользователь все еще привязан к какой-то *другой* активной игре (не той, что только что очистили)
// Это может случиться, если _cleanupPreviousPendingGameForUser не нашла ожидающую, но он в другой игре.
const stillExistingGameId = this.userIdentifierToGameId[identifier];
if (stillExistingGameId && this.games[stillExistingGameId] && !this.games[stillExistingGameId].gameState?.isGameOver) {
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
this.handleRequestGameState(socket, identifier);
return;
}
const gameId = uuidv4();
console.log(`[GameManager.createGame] New 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] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} in game ${gameId}. Cleaning up.`);
this._cleanupGame(gameId, 'player_info_missing_after_add_on_create');
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
return;
}
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated.`);
socket.emit('gameCreated', {
gameId: gameId,
mode: mode,
yourPlayerId: assignedPlayerId,
chosenCharacterKey: actualCharacterKey
});
if (mode === 'ai') {
if (game.initializeGame()) {
game.startGame();
} else {
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
}
} else if (mode === 'pvp') {
if (game.initializeGame()) { // Для PvP инициализируем даже с одним игроком
if (!this.pendingPvPGames.includes(gameId)) {
this.pendingPvPGames.push(gameId);
}
socket.emit('waitingForOpponent');
this.broadcastAvailablePvPGames();
} else {
this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player');
}
}
} else {
console.error(`[GameManager.createGame] game.addPlayer failed for ${identifier} in ${gameId}. Cleaning up.`);
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create');
}
}
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`);
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;
}
// Нельзя присоединиться к своей же игре, если ты ее владелец и не отключен временно
if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
// Это может быть ситуация, когда он уже в этой игре (например, обновил страницу и пытается "присоединиться" к своей же)
// handleRequestGameState должен корректно обработать реконнект
console.warn(`[GameManager.joinGame] User ${identifier} trying to join their own game ${gameIdToJoin} as a new player. Treating as reconnect request.`);
this.handleRequestGameState(socket, identifier);
return;
}
// 1. Проверить, не находится ли пользователь уже в ЗАВЕРШЕННОЙ, но не очищенной игре.
const currentActiveGameId = this.userIdentifierToGameId[identifier];
if (currentActiveGameId && this.games[currentActiveGameId] && this.games[currentActiveGameId].gameState?.isGameOver) {
console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameId} while trying to join ${gameIdToJoin}. Cleaning old one.`);
this._cleanupGame(currentActiveGameId, `stale_finished_on_join_attempt_${identifier}`);
}
// 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться),
// и это НЕ его собственная ожидающая PvP игра, то отказать.
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить.
if (currentActiveGameId && currentActiveGameId !== gameIdToJoin && this.games[currentActiveGameId] && !this.games[currentActiveGameId].gameState?.isGameOver) {
const usersCurrentGame = this.games[currentActiveGameId];
if (usersCurrentGame.mode === 'pvp' &&
usersCurrentGame.playerCount === 1 &&
usersCurrentGame.ownerIdentifier === identifier &&
this.pendingPvPGames.includes(currentActiveGameId)) {
console.log(`[GameManager.joinGame] User ${identifier} is owner of pending game ${currentActiveGameId}, but wants to join ${gameIdToJoin}. Cleaning up old game.`);
this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`);
} else {
// Пользователь в другой активной игре (не своей ожидающей)
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
return;
}
}
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] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
// Здесь важно НЕ удалять gameToJoin, так как в ней мог быть первый игрок.
// addPlayer должен был вернуть false и не изменить playerCount игры, если что-то пошло не так критично.
// Если addPlayer вернул true, но инфо нет, это проблема в addPlayer или GameInstance.
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
// Возможно, стоит удалить игрока из userIdentifierToGameId, если он не смог корректно добавиться
if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier];
return;
}
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}.`);
socket.emit('gameCreated', { // Используем gameCreated для консистентности, т.к. клиент ожидает это для установки ID игры
gameId: gameIdToJoin,
mode: gameToJoin.mode,
yourPlayerId: joinedPlayerInfo.id,
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
});
if (gameToJoin.playerCount === 2) {
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
// Убедимся, что игра еще раз инициализируется с обоими игроками, если нужно
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 {
// addPlayer вернул false, GameInstance должен был отправить причину через gameError
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`);
}
}
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
// 1. Проверить, не находится ли пользователь уже в ЗАВЕРШЕННОЙ, но не очищенной игре.
const existingGameId = this.userIdentifierToGameId[identifier];
if (existingGameId && this.games[existingGameId]) {
const existingGame = this.games[existingGameId];
if (existingGame.gameState && existingGame.gameState.isGameOver) {
this._cleanupGame(existingGameId, `stale_finished_on_find_random_${identifier}`);
} else {
// Если он уже в активной игре (своей ожидающей или другой)
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
this.handleRequestGameState(socket, identifier); return;
}
}
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую.
this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`);
// 3. Если после очистки пользователь все еще привязан к какой-то *другой* активной игре
const stillExistingGameId = this.userIdentifierToGameId[identifier];
if (stillExistingGameId && this.games[stillExistingGameId] && !this.games[stillExistingGameId].gameState?.isGameOver) {
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
this.handleRequestGameState(socket, identifier);
return;
}
let gameIdToJoin = null;
// Итерируем копию массива, чтобы избежать проблем при удалении элементов из pendingPvPGames внутри цикла
for (const id of [...this.pendingPvPGames]) {
const pendingGame = this.games[id];
if (pendingGame && pendingGame.mode === 'pvp' &&
pendingGame.playerCount === 1 &&
pendingGame.ownerIdentifier !== identifier && // Не присоединяться к своей же игре через "случайный поиск"
!pendingGame.gameState?.isGameOver) {
gameIdToJoin = id; break;
} else if (!pendingGame || pendingGame.gameState?.isGameOver) { // Очистка "мертвых" ожидающих игр
// Это может случиться, если игра была удалена, но ID остался в pendingPvPGames
console.warn(`[GameManager.findRandomPvPGame] Found stale/finished pending game ${id}. Cleaning up.`);
this._cleanupGame(id, `stale_finished_pending_on_find_random`);
}
}
if (gameIdToJoin) {
console.log(`[GameManager.findRandomPvPGame] Found pending game ${gameIdToJoin} for ${identifier}. Joining...`);
const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)]; // TODO: Сделать выбор персонажа более умным
this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey);
} else {
console.log(`[GameManager.findRandomPvPGame] No suitable pending game. Creating new PvP game for ${identifier}.`);
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
}
}
handlePlayerAction(identifier, actionData) {
const gameId = this.userIdentifierToGameId[identifier];
// console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, GameID: ${gameId}`);
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] Action from ${identifier} for game ${gameId}, but game is over. Requesting state.`);
this.handleRequestGameState(playerSocket, identifier);
}
return;
}
game.processPlayerAction(identifier, actionData);
} else {
console.warn(`[GameManager.handlePlayerAction] No game found for user ${identifier} (mapped to game ${gameId}). Clearing map entry.`);
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] User: ${identifier} surrendered. GameID from map: ${gameId}`);
const game = this.games[gameId];
if (game) {
if (game.gameState?.isGameOver) {
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`);
// Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это, если игра действительно должна быть удалена.
return;
}
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
else { console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} missing playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); }
} else {
console.warn(`[GameManager.handlePlayerSurrender] No game found for user ${identifier}. Clearing map entry.`);
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
}
}
handleLeaveAiGame(identifier) {
const gameId = this.userIdentifierToGameId[identifier];
console.log(`[GameManager.handleLeaveAiGame] User: ${identifier} leaving AI game. GameID from map: ${gameId}`);
const game = this.games[gameId];
if (game) {
if (game.gameState?.isGameOver) {
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} game ${gameId} leaving, but game ALREADY OVER.`);
return;
}
if (game.mode === 'ai') {
if (typeof game.playerExplicitlyLeftAiGame === 'function') {
game.playerExplicitlyLeftAiGame(identifier);
} else {
console.error(`[GameManager.handleLeaveAiGame] CRITICAL: GameInstance ${gameId} missing playerExplicitlyLeftAiGame! Cleaning up directly.`);
this._cleanupGame(gameId, "leave_ai_missing_method_gm");
}
} else {
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
}
} else {
console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`);
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
}
}
_findClientSocketByIdentifier(identifier) {
for (const sid of this.io.sockets.sockets.keys()) {
const s = this.io.sockets.sockets.get(sid);
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}, User: ${identifier}, GameID from map: ${gameIdFromMap}`);
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
if (game) {
if (game.gameState?.isGameOver) {
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER. Game will be cleaned up by its own logic or next relevant action.`);
// Не удаляем из userIdentifierToGameId здесь, пусть это сделает _cleanupGame по завершению игры.
return;
}
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) {
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active player ${identifier} (Role: ${playerInfoInGame.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey);
} else {
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} missing handlePlayerPotentiallyLeft!`);
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm");
}
} else if (playerInfoInGame && playerInfoInGame.socket?.id !== socketId) {
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier}. Active socket in game: ${playerInfoInGame.socket?.id}. No action taken by GM.`);
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while ALREADY temp disconnected. Reconnect timer in GameInstance handles final cleanup.`);
} else if (!playerInfoInGame) {
// Это странная ситуация: пользователь привязан к игре, но его нет в списке игроков этой игры.
// Это может случиться, если игра была очищена, но userIdentifierToGameId не обновился.
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. This might indicate a stale userIdentifierToGameId entry. Clearing map for this user.`);
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
delete this.userIdentifierToGameId[identifier];
}
}
} else {
// Если игра не найдена, но пользователь был к ней привязан, значит, карта устарела.
if (this.userIdentifierToGameId[identifier]) {
console.warn(`[GameManager.handleDisconnect] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
delete this.userIdentifierToGameId[identifier];
}
}
}
_cleanupGame(gameId, reason = 'unknown') {
console.log(`[GameManager._cleanupGame] Attempting cleanup for GameID: ${gameId}, Reason: ${reason}`);
const game = this.games[gameId];
if (!game) {
// Если игры уже нет в this.games, но она есть в pendingPvPGames или userIdentifierToGameId,
// нужно все равно почистить эти структуры.
console.warn(`[GameManager._cleanupGame] Game instance for ${gameId} not found in this.games. Cleaning up associated records.`);
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) {
this.pendingPvPGames.splice(pendingIdx, 1);
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
}
for (const idKey in this.userIdentifierToGameId) {
if (this.userIdentifierToGameId[idKey] === gameId) {
delete this.userIdentifierToGameId[idKey];
console.log(`[GameManager._cleanupGame] Removed mapping for user ${idKey} to game ${gameId}.`);
}
}
this.broadcastAvailablePvPGames(); // Обновляем список, так как ожидающая игра могла быть удалена
return false; // Игры не было для основной очистки
}
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${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] Marking game ${game.id} as game over.`);
game.gameState.isGameOver = true;
// Можно также отправить финальное событие gameOver, если это не было сделано ранее
// game.io.to(game.id).emit('gameOver', { /* ... данные ... */ });
}
// Очищаем всех игроков этой игры из глобальной карты userIdentifierToGameId
Object.values(game.players).forEach(pInfo => {
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
delete this.userIdentifierToGameId[pInfo.identifier];
console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for player ${pInfo.identifier}.`);
}
});
// Дополнительная проверка для ownerIdentifier, если он не был в game.players
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] Cleared userIdentifierToGameId for owner ${game.ownerIdentifier} (was not in players list).`);
}
}
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) {
this.pendingPvPGames.splice(pendingIdx, 1);
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
}
delete this.games[gameId];
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. Pending: ${this.pendingPvPGames.length}. User map size: ${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.socket?.userData) { // Приоритет данным из сокета, если есть
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){ // Фоллбэк на данные по ownerId
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 || 'Не выбран';
} else if (p1Entry) { // Если есть p1Entry, но нет userData (маловероятно для создателя)
p1Username = `User#${String(p1Entry.identifier).substring(0,4)}`;
const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey);
p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран';
}
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
} else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) {
// Если игра есть в pending, но не соответствует условиям, ее нужно оттуда удалить
console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is in pendingPvPGames but is not a valid pending game (players: ${game.playerCount}, over: ${game.gameState?.isGameOver}). Removing.`);
this._cleanupGame(gameId, 'invalid_pending_game_in_list'); // Это вызовет broadcastAvailablePvPGames снова, поэтому мы делаем map на копии.
}
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] User: ${identifier} (Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
if (game) {
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
// console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo in game.players: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
if (playerInfoInGame) { // Игрок действительно является частью этой игры
if (game.gameState?.isGameOver) {
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
// _cleanupGame должна быть вызвана, когда игра фактически завершается,
// здесь мы не должны удалять из userIdentifierToGameId, если игра еще есть в this.games.
// Если игра уже очищена, то game будет null.
return;
}
if (typeof game.handlePlayerReconnected === 'function') {
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
// reconnected может вернуть false, если реконнект не удался по внутренней причине GameInstance
if (!reconnected) {
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`);
// GameInstance должен был отправить ошибку клиенту, если нужно.
// Можно рассмотреть _handleGameRecoveryError, если это критично.
}
} else {
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request');
}
} else {
// Пользователь привязан к этой игре в userIdentifierToGameId, но его нет в списке игроков game.players
// Это может означать, что он был удален из игры (например, таймаут реконнекта), но userIdentifierToGameId не очистился.
console.warn(`[GameManager.handleRequestGameState] User ${identifier} mapped to game ${gameIdFromMap}, but NOT FOUND in game.players. Cleaning map & sending gameNotFound.`);
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_but_mapped_on_request');
}
} else {
// Игра не найдена в this.games, но могла быть в userIdentifierToGameId
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
if (this.userIdentifierToGameId[identifier]) { // Если привязка была, удаляем ее
console.warn(`[GameManager.handleRequestGameState] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
delete this.userIdentifierToGameId[identifier];
}
}
}
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${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];
if (this.games[problematicGameIdForUser]) { // Если она все же есть (маловероятно, если gameId был null)
this._cleanupGame(problematicGameIdForUser, `recovery_error_stale_map_gm_${identifier}_reason_${reasonCode}`);
} else { // Если ее нет, просто чистим карту
delete this.userIdentifierToGameId[identifier];
}
}
// Если после _cleanupGame пользователь все еще привязан (маловероятно, но для гарантии)
if (this.userIdentifierToGameId[identifier]) {
delete this.userIdentifierToGameId[identifier];
}
// Отправляем gameNotFound, чтобы клиент перешел на экран логина или выбора игры
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' });
}
}
module.exports = GameManager;