bc/server/game/GameManager.js

553 lines
38 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.");
}
_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] User ${identifier} has an existing pending PvP game ${oldPendingGameId}. Removing it. Reason: ${reasonSuffix}`);
this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_pvp_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'}`);
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
// 1. Проверить, не находится ли пользователь уже в какой-либо АКТИВНОЙ игре.
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
const existingGame = this.games[existingGameIdForUser];
if (existingGame.gameState && existingGame.gameState.isGameOver) {
console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up before creating new.`);
this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`);
// После _cleanupGame, existingGameIdForUser в userIdentifierToGameId[identifier] должен быть удален
} else {
// Пользователь в активной игре.
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, и он пытается создать НОВУЮ (любую), то ее нужно будет удалить ниже.
// Если это ДРУГАЯ активная игра (не его ожидающая PvP), то отказать.
const isHisOwnPendingPvp = existingGame.mode === 'pvp' &&
existingGame.ownerIdentifier === identifier &&
existingGame.playerCount === 1 &&
this.pendingPvPGames.includes(existingGameIdForUser);
if (!isHisOwnPendingPvp) {
// Он в другой активной игре (AI, или PvP с оппонентом, или PvP другого игрока)
console.warn(`[GameManager.createGame] User ${identifier} is already in an active game ${existingGameIdForUser} (mode: ${existingGame.mode}, owner: ${existingGame.ownerIdentifier}). Cannot create new.`);
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
return;
}
// Если это его ожидающая PvP, то _cleanupPreviousPendingGameForUser ниже ее удалит.
}
}
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру.
// Это важно сделать ДО создания новой игры, чтобы освободить userIdentifierToGameId.
const cleanedUp = this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`);
if (cleanedUp) {
console.log(`[GameManager.createGame] Successfully cleaned up previous pending PvP game for ${identifier}.`);
} else {
console.log(`[GameManager.createGame] No previous pending PvP game found or needed cleanup for ${identifier}.`);
}
console.log(`[GameManager.createGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
// 3. Окончательная проверка: если ПОСЛЕ очистки пользователь все еще привязан к какой-то активной игре
// (Это может случиться, если _cleanupPreviousPendingGameForUser не нашла ожидающую, но он был в другой игре, что было бы ошибкой логики выше)
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
console.error(`[GameManager.createGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying creation.`);
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. Current map for ${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 game ${gameId} initialized by GameManager, starting...`);
game.startGame();
} else {
console.error(`[GameManager.createGame] AI game ${gameId} init failed in GameManager. Cleaning up.`);
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 {
console.error(`[GameManager.createGame] PvP game ${gameId} (single player) init failed. Cleaning up.`);
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');
// game.addPlayer должен был сам отправить ошибку клиенту
}
}
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) {
console.warn(`[GameManager.joinGame] User ${identifier} trying to join their own game ${gameIdToJoin} where they are owner and not disconnected. Treating as reconnect request.`);
this.handleRequestGameState(socket, identifier);
return;
}
// 1. Очистка завершенной игры пользователя, если такая есть
const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier];
if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) {
console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameIdUserIsIn} while trying to join ${gameIdToJoin}. Cleaning old one.`);
this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`);
}
// 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться),
// и это НЕ его собственная ожидающая PvP игра, то отказать.
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить.
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] User ${identifier} is owner of pending game ${stillExistingGameIdForUser}, but wants to join ${gameIdToJoin}. Cleaning up old game.`);
this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`);
} else {
// Пользователь в другой активной игре (не своей ожидающей)
console.warn(`[GameManager.joinGame] User ${identifier} is in another active game ${stillExistingGameIdForUser}. Cannot join ${gameIdToJoin}.`);
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
return;
}
}
console.log(`[GameManager.joinGame] After potential cleanup before join, user ${identifier} mapping: ${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] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier];
return;
}
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}. User map updated. Current map for ${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] 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 {
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`);
// GameInstance должен был отправить причину
}
}
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${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] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up.`);
this._cleanupGame(existingGameIdForUser, `stale_finished_on_find_random_${identifier}`);
} else {
console.warn(`[GameManager.findRandomPvPGame] User ${identifier} is already in an active/pending game ${existingGameIdForUser}. Cannot find random.`);
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
this.handleRequestGameState(socket, identifier); return;
}
}
// Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую.
this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`);
console.log(`[GameManager.findRandomPvPGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
// Если после очистки пользователь все еще привязан к какой-то *другой* активной игре
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
console.error(`[GameManager.findRandomPvPGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying find random.`);
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] 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)];
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];
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);
} else {
console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, game over, but no socket found for user.`);
this._cleanupGame(gameId, `action_on_over_no_socket_gm_${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}).`);
socket.emit('gameError', { message: 'Вы не в AI игре.' }); // Сообщить клиенту об ошибке
}
} else {
console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`);
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()) { // Использование .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}, 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.`);
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) {
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) {
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.`);
}
// Важно: итерируем по ключам, так как удаление может изменить объект
Object.keys(this.userIdentifierToGameId).forEach(idKey => {
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 because it's being cleaned up while active.`);
game.gameState.isGameOver = true;
// Можно рассмотреть отправку gameOver, если игра прерывается извне
// game.io.to(game.id).emit('gameOver', { 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] Cleared userIdentifierToGameId for player ${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] 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() {
// Итерируем копию массива pendingPvPGames, так как _cleanupGame может его изменять
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; // Это должен быть identifier создателя
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){
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
const ownerCharKey = game.playerCharacterKey; // Это ключ персонажа для роли PLAYER_ID в этой игре
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] 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');
}
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);
if (playerInfoInGame) {
if (game.gameState?.isGameOver) {
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
// _cleanupGame будет вызвана, когда игра фактически завершается.
// Здесь не удаляем из userIdentifierToGameId, если игра еще есть в this.games.
return;
}
if (typeof game.handlePlayerReconnected === 'function') {
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
if (!reconnected) {
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`);
// GameInstance должен был отправить ошибку.
}
} 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 {
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 {
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];
// Если игра была удалена, но пользователь к ней привязан, просто чистим карту
delete this.userIdentifierToGameId[identifier];
console.log(`[GameManager._handleGameRecoveryError] Cleaned stale userIdentifierToGameId[${identifier}] pointing to ${problematicGameIdForUser}.`);
}
// Убедимся, что после всех очисток пользователь точно не привязан
if (this.userIdentifierToGameId[identifier]) {
delete this.userIdentifierToGameId[identifier];
console.warn(`[GameManager._handleGameRecoveryError] Force cleaned userIdentifierToGameId[${identifier}] as a final measure.`);
}
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' });
}
}
module.exports = GameManager;