bc/server/game/GameManager.js

602 lines
45 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 игр, ожидающих второго игрока
console.log("[GameManager] Initialized.");
}
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
console.log(`[GameManager._removePreviousPendingGames] Called for user: ${identifier}, currentSocket: ${currentSocketId}, excludeGameId: ${excludeGameId}`);
const oldPendingGameId = this.userIdentifierToGameId[identifier];
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
const gameToRemove = this.games[oldPendingGameId];
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
const playerInfo = Object.values(gameToRemove.players).find(p => p.identifier === identifier);
if (playerInfo && playerInfo.id === GAME_CONFIG.PLAYER_ID) {
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} (socket: ${currentSocketId}) created/joined new game. Removing their previous owned pending PvP game: ${oldPendingGameId}`);
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game_creation_or_join');
} else {
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} had pending game ${oldPendingGameId}, but was not the primary player. Not removing.`);
}
}
} else {
console.log(`[GameManager._removePreviousPendingGames] No old pending game found for user ${identifier} or conditions not met.`);
}
}
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey}`);
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
console.warn(`[GameManager.createGame] User ${identifier} already in game ${this.userIdentifierToGameId[identifier]}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
// Если игра существует и НЕ завершена
if (!existingGame.gameState?.isGameOver) {
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
} else {
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
}
this.handleRequestGameState(socket, identifier); // Попытка восстановить сессию в существующей игре
return;
} else {
// Игра существует, но завершена. GameManager должен был ее очистить.
// Если мы здесь, значит, что-то пошло не так с очисткой.
console.warn(`[GameManager.createGame] User ${identifier} was mapped to an already finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up stale entry before creating new game.`);
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_create_for_${identifier}`);
// this.userIdentifierToGameId[identifier] будет удален в _cleanupGame
}
}
this._removePreviousPendingGames(socket.id, identifier);
const gameId = uuidv4();
console.log(`[GameManager.createGame] Generated new GameID: ${gameId}`);
const game = new GameInstance(gameId, this.io, mode, this);
this.games[gameId] = game;
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
this.userIdentifierToGameId[identifier] = gameId;
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
const assignedPlayerId = Object.values(game.players).find(p=>p.identifier === identifier)?.id;
if (!assignedPlayerId) {
console.error(`[GameManager.createGame] CRITICAL: Failed to assign player role for user ${identifier} in game ${gameId}.`);
this._cleanupGame(gameId, 'player_add_failed_no_role_assigned');
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось присвоить роль).' });
return;
}
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
console.log(`[GameManager.createGame] Emitted 'gameCreated' to ${identifier}. gameId: ${gameId}, yourPlayerId: ${assignedPlayerId}`);
if (mode === 'ai') {
const isInitialized = game.initializeGame();
if (isInitialized) {
console.log(`[GameManager.createGame] AI game ${gameId} initialized, starting game...`);
game.startGame();
} else {
console.error(`[GameManager.createGame] AI game ${gameId} initialization failed. Cleaning up.`);
this._cleanupGame(gameId, 'initialization_failed_on_ai_create');
}
} else if (mode === 'pvp') {
if (!this.pendingPvPGames.includes(gameId)) {
this.pendingPvPGames.push(gameId);
console.log(`[GameManager.createGame] PvP game ${gameId} added to pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
}
game.initializeGame();
console.log(`[GameManager.createGame] PvP game ${gameId} initialized (or placeholder). Emitting 'waitingForOpponent'.`);
socket.emit('waitingForOpponent');
this.broadcastAvailablePvPGames();
}
} else {
console.error(`[GameManager.createGame] game.addPlayer failed for user ${identifier} in game ${gameId}. Cleaning up.`);
this._cleanupGame(gameId, 'player_add_failed_in_instance');
}
}
joinGame(socket, gameIdToJoin, identifier) {
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join GameID: ${gameIdToJoin}`);
const game = this.games[gameIdToJoin];
if (!game) {
console.warn(`[GameManager.joinGame] Game ${gameIdToJoin} not found for user ${identifier}.`);
socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return;
}
if (game.gameState?.isGameOver) {
console.warn(`[GameManager.joinGame] User ${identifier} tried to join game ${gameIdToJoin} which is already over.`);
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
this._cleanupGame(gameIdToJoin, `attempt_to_join_finished_game_${identifier}`);
return;
}
if (game.mode !== 'pvp') {
console.warn(`[GameManager.joinGame] User ${identifier} tried to join non-PvP game ${gameIdToJoin}. Mode: ${game.mode}`);
socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP режим).' }); return;
}
if (game.playerCount >= 2 && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
console.warn(`[GameManager.joinGame] User ${identifier} tried to join full PvP game ${gameIdToJoin}. Players: ${game.playerCount}`);
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
}
if (game.ownerIdentifier === identifier && !Object.values(game.players).some(p => p.identifier === identifier && p.isTemporarilyDisconnected)) {
console.warn(`[GameManager.joinGame] User ${identifier} (owner) tried to join their own waiting game ${gameIdToJoin} as a new player.`);
socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете, как новый игрок.' });
this.handleRequestGameState(socket, identifier); return;
}
if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameIdToJoin) {
const otherGame = this.games[this.userIdentifierToGameId[identifier]];
if (otherGame && !otherGame.gameState?.isGameOver) {
console.warn(`[GameManager.joinGame] User ${identifier} already in another active game: ${this.userIdentifierToGameId[identifier]}. Cannot join ${gameIdToJoin}.`);
socket.emit('gameError', { message: 'Вы уже находитесь в другой игре. Сначала завершите или покиньте её.' });
this.handleRequestGameState(socket, identifier); return;
} else if (otherGame && otherGame.gameState?.isGameOver) {
console.warn(`[GameManager.joinGame] User ${identifier} was mapped to a finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up before join.`);
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_join_${identifier}`);
}
}
this._removePreviousPendingGames(socket.id, identifier, gameIdToJoin);
if (game.addPlayer(socket, null, identifier)) {
this.userIdentifierToGameId[identifier] = gameIdToJoin;
console.log(`[GameManager.joinGame] Player ${identifier} successfully added/reconnected to PvP game ${gameIdToJoin}. User map updated: userIdentifierToGameId[${identifier}] = ${this.userIdentifierToGameId[identifier]}`);
if (game.playerCount === 2) {
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full with 2 active players. Initializing and starting.`);
const isInitialized = game.initializeGame();
if (isInitialized) {
game.startGame();
} else {
console.error(`[GameManager.joinGame] PvP game ${gameIdToJoin} initialization failed after 2nd player join. Cleaning up.`);
this._cleanupGame(gameIdToJoin, 'initialization_failed_on_pvp_join'); return;
}
const idx = this.pendingPvPGames.indexOf(gameIdToJoin);
if (idx > -1) {
this.pendingPvPGames.splice(idx, 1);
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} removed from pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
}
this.broadcastAvailablePvPGames();
} else {
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} has ${game.playerCount} active players after join/reconnect. Waiting for more or game was already running.`);
}
} else {
console.error(`[GameManager.joinGame] game.addPlayer failed for user ${identifier} trying to join ${gameIdToJoin}.`);
}
}
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
console.log(`[GameManager.findAndJoinRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
const existingGame = this.games[this.userIdentifierToGameId[identifier]];
if (existingGame && !existingGame.gameState?.isGameOver) {
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} already in active game: ${this.userIdentifierToGameId[identifier]}.`);
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
this.handleRequestGameState(socket, identifier); return;
} else if (existingGame && existingGame.gameState?.isGameOver) {
console.warn(`[GameManager.findAndJoinRandomPvPGame] User ${identifier} mapped to finished game ${this.userIdentifierToGameId[identifier]}. Cleaning up.`);
this._cleanupGame(this.userIdentifierToGameId[identifier], `stale_finished_game_on_find_random_${identifier}`);
}
}
this._removePreviousPendingGames(socket.id, identifier);
let gameIdToJoin = null;
console.log(`[GameManager.findAndJoinRandomPvPGame] Searching pending games for ${identifier}. Current pending: ${this.pendingPvPGames.join(', ')}`);
for (const id of [...this.pendingPvPGames]) { // Iterate over a copy in case of modification
const pendingGame = this.games[id];
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
if (!pendingGame.gameState || !pendingGame.gameState.isGameOver) {
gameIdToJoin = id;
console.log(`[GameManager.findAndJoinRandomPvPGame] Found suitable pending game: ${gameIdToJoin} for user ${identifier}.`);
break;
} else {
console.log(`[GameManager.findAndJoinRandomPvPGame] Pending game ${id} is already over. Skipping and cleaning.`);
this._cleanupGame(id, `stale_finished_pending_game_during_find_random`); // Clean up stale finished game
}
}
}
if (gameIdToJoin) {
this.joinGame(socket, gameIdToJoin, identifier);
} else {
console.log(`[GameManager.findAndJoinRandomPvPGame] No suitable pending game found for ${identifier}. Creating a new PvP game.`);
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
}
}
handlePlayerAction(identifier, actionData) {
const gameId = this.userIdentifierToGameId[identifier];
console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, AbilityID: ${actionData?.abilityId}, GameID from map: ${gameId}`);
const game = this.games[gameId];
if (game) {
if (game.gameState?.isGameOver) {
console.warn(`[GameManager.handlePlayerAction] User ${identifier} in game ${gameId} attempted action, but game is ALREADY OVER. Action ignored.`);
game.playerSockets[identifier]?.socket.emit('gameError', {message: "Действие невозможно: игра уже завершена."});
// Potentially send gameNotFound or re-send gameOver if client missed it
this.handleRequestGameState(game.playerSockets[identifier]?.socket || this._findClientSocketByIdentifier(identifier), identifier);
return;
}
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
if (playerInfo && playerInfo.socket && playerInfo.socket.connected && !playerInfo.isTemporarilyDisconnected) {
console.log(`[GameManager.handlePlayerAction] Forwarding action from user ${identifier} (Socket: ${playerInfo.socket.id}) to game ${gameId}.`);
game.processPlayerAction(playerInfo.socket.id, actionData);
} else if (playerInfo && playerInfo.isTemporarilyDisconnected) {
console.warn(`[GameManager.handlePlayerAction] User ${identifier} (Socket: ${playerInfo.socket?.id}) in game ${gameId} attempted action, but is temporarily disconnected. Action ignored.`);
playerInfo.socket?.emit('gameError', {message: "Действие невозможно: вы временно отключены."});
} else if (playerInfo && playerInfo.socket && !playerInfo.socket.connected) {
console.warn(`[GameManager.handlePlayerAction] User ${identifier} (Socket: ${playerInfo.socket.id}) in game ${gameId} attempted action, but socket is reported as disconnected by server. Action ignored. Potential state mismatch.`);
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
}
} else {
console.warn(`[GameManager.handlePlayerAction] User ${identifier} attempted action for game ${gameId}, but active player info or socket not found in game instance. Removing from user map.`);
delete this.userIdentifierToGameId[identifier];
const clientSocket = this._findClientSocketByIdentifier(identifier);
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока при действии).' });
}
} else {
console.warn(`[GameManager.handlePlayerAction] User ${identifier} attempted action, but game ${gameId} (from map) not found in this.games. Removing from user map.`);
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} tried to surrender in game ${gameId} which is ALREADY OVER. Ignoring.`);
// _cleanupGame should have already run or will run.
// Ensure map is clear if it's somehow stale.
if (this.userIdentifierToGameId[identifier] === gameId) {
console.warn(`[GameManager.handlePlayerSurrender] Stale map entry for ${identifier} to finished game ${gameId}. Cleaning.`);
delete this.userIdentifierToGameId[identifier]; // Direct cleanup if game is confirmed over.
}
return;
}
if (typeof game.playerDidSurrender === 'function') {
console.log(`[GameManager.handlePlayerSurrender] Forwarding surrender from user ${identifier} to game ${gameId}.`);
game.playerDidSurrender(identifier); // This method will call _cleanupGame internally
} else {
console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} is missing playerDidSurrender method! Attempting fallback cleanup for PvP.`);
if (game.mode === 'pvp' && game.gameState && !game.gameState.isGameOver) {
const surrenderedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
if (surrenderedPlayerInfo) {
const opponentInfo = Object.values(game.players).find(p => p.identifier !== identifier && !p.isTemporarilyDisconnected);
const winnerRole = opponentInfo ? opponentInfo.id : null;
if (typeof game.endGameDueToDisconnect === 'function') {
game.endGameDueToDisconnect(surrenderedPlayerInfo.id, surrenderedPlayerInfo.chosenCharacterKey, "opponent_surrendered_fallback", winnerRole);
} else {
this._cleanupGame(gameId, "surrender_fallback_cleanup_missing_end_method");
}
} else {
this._cleanupGame(gameId, "surrender_player_not_found_in_game_instance_fallback");
}
} else if (game.mode === 'ai') {
console.log(`[GameManager.handlePlayerSurrender] User ${identifier} in AI game ${gameId} surrendered. No playerDidSurrender. Disconnect will handle.`);
}
}
} else {
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} surrendered, but game ${gameId} (from map) not found in this.games. User map might be stale or already cleaned up.`);
if (this.userIdentifierToGameId[identifier] === gameId || this.userIdentifierToGameId[identifier]) {
console.log(`[GameManager.handlePlayerSurrender] Clearing map entry for ${identifier} which pointed to ${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) {
// console.log(`[GameManager._findClientSocketByIdentifier] Found active socket ${s.id} for identifier ${identifier}.`);
return s;
}
}
// console.log(`[GameManager._findClientSocketByIdentifier] No active socket found for identifier ${identifier}.`);
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 && game.gameState.isGameOver) {
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket: ${socketId}) is ALREADY OVER. Disconnect processing skipped for game logic.`);
// _cleanupGame (called by playerDidSurrender or other end game methods) is responsible for clearing userIdentifierToGameId.
// If the map entry still exists, it implies _cleanupGame might not have completed or there's a race condition.
// However, we shouldn't initiate new game logic like handlePlayerPotentiallyLeft.
return;
}
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} found (and not game over). PlayerInfo for user ${identifier}: ${playerInfo ? `Role: ${playerInfo.id}, CurrentSocketInGame: ${playerInfo.socket?.id}, IsTempDisconnected: ${playerInfo.isTemporarilyDisconnected}` : 'Not found in game instance'}`);
if (playerInfo && playerInfo.socket && playerInfo.socket.id === socketId && !playerInfo.isTemporarilyDisconnected) {
console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active socket for user ${identifier} (Role: ${playerInfo.id}) in game ${gameIdFromMap}. Notifying GameInstance.`);
if (typeof game.handlePlayerPotentiallyLeft === 'function') {
game.handlePlayerPotentiallyLeft(playerInfo.id, playerInfo.identifier, playerInfo.chosenCharacterKey);
} else {
console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} is missing handlePlayerPotentiallyLeft method!`);
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_in_instance_on_disconnect");
}
} else if (playerInfo && playerInfo.socket && playerInfo.socket.id !== socketId) {
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier} (active socket in game ${gameIdFromMap} is ${playerInfo.socket.id}). Ignoring this disconnect for game logic.`);
} else if (playerInfo && playerInfo.isTemporarilyDisconnected && playerInfo.socket?.id === socketId) {
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already being temporarily disconnected. Reconnect timer should handle final cleanup.`);
} else if (!playerInfo && gameIdFromMap) {
console.log(`[GameManager.handleDisconnect] User ${identifier} was mapped to game ${gameIdFromMap}, but not found in game.players.`);
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
console.warn(`[GameManager.handleDisconnect] Removing stale map entry for ${identifier} to game ${gameIdFromMap} where player was not found in instance.`);
delete this.userIdentifierToGameId[identifier];
}
}
} else {
console.log(`[GameManager.handleDisconnect] User ${identifier} (Socket: ${socketId}) disconnected, but no active game instance found for gameId ${gameIdFromMap} (or gameId was undefined).`);
if (this.userIdentifierToGameId[identifier]) { // If a mapping exists, even if gameIdFromMap was undefined or game not in this.games
console.warn(`[GameManager.handleDisconnect] Removing map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]}.`);
delete this.userIdentifierToGameId[identifier];
}
}
}
_cleanupGame(gameId, reason = 'unknown') {
console.log(`[GameManager._cleanupGame] Attempting to cleanup GameID: ${gameId}, Reason: ${reason}`);
const game = this.games[gameId];
if (!game) {
console.warn(`[GameManager._cleanupGame] Game ${gameId} not found in this.games. Checking pending list and user map.`);
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) {
this.pendingPvPGames.splice(pendingIdx, 1);
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pending list (instance was already gone). Reason: ${reason}. Current pending: ${this.pendingPvPGames.join(', ')}`);
this.broadcastAvailablePvPGames();
}
// Ensure any lingering user map entries for this non-existent game are cleared.
let mapCleaned = false;
for (const idKey in this.userIdentifierToGameId) {
if (this.userIdentifierToGameId[idKey] === gameId) {
console.log(`[GameManager._cleanupGame] Clearing STALE mapping for user ${idKey} to non-existent game ${gameId}.`);
delete this.userIdentifierToGameId[idKey];
mapCleaned = true;
}
}
if (mapCleaned) console.log(`[GameManager._cleanupGame] Stale user maps cleared for game ${gameId}.`);
return false; // Game was not found in this.games
}
console.log(`[GameManager._cleanupGame] Cleaning up game ${gameId}. Owner: ${game.ownerIdentifier}. Reason: ${reason}.`);
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
if (typeof game.clearAllReconnectTimers === 'function') {
game.clearAllReconnectTimers();
console.log(`[GameManager._cleanupGame] Called clearAllReconnectTimers for game ${gameId}.`);
}
// Ensure gameState is marked as over if not already
if (game.gameState && !game.gameState.isGameOver) {
console.warn(`[GameManager._cleanupGame] Game ${gameId} was not marked as 'isGameOver' during cleanup. Marking now. Reason: ${reason}`);
game.gameState.isGameOver = true;
}
// Remove all players of this game from the global userIdentifierToGameId map
let playersInGameCleaned = 0;
Object.values(game.players).forEach(pInfo => {
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
console.log(`[GameManager._cleanupGame] Deleting mapping for player ${pInfo.identifier} (Role: ${pInfo.id}) from game ${gameId}.`);
delete this.userIdentifierToGameId[pInfo.identifier];
playersInGameCleaned++;
}
});
// Also check the owner, in case they weren't in game.players (e.g., game created but owner disconnected before fully joining)
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
if (!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) { // Only if not already cleaned
console.log(`[GameManager._cleanupGame] Deleting mapping for owner ${game.ownerIdentifier} from game ${gameId} (was not in game.players or already cleaned).`);
delete this.userIdentifierToGameId[game.ownerIdentifier];
playersInGameCleaned++;
}
}
if (playersInGameCleaned > 0) {
console.log(`[GameManager._cleanupGame] ${playersInGameCleaned} player mappings cleared for game ${gameId}.`);
}
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) {
this.pendingPvPGames.splice(pendingIdx, 1);
console.log(`[GameManager._cleanupGame] Game ${gameId} removed from pending list. Current pending: ${this.pendingPvPGames.join(', ')}`);
}
delete this.games[gameId];
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
this.broadcastAvailablePvPGames(); // Update list for all clients
return true;
}
getAvailablePvPGamesListForClient() {
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Generating list from pending: ${this.pendingPvPGames.join(', ')}`);
return this.pendingPvPGames
.map(gameId => {
const game = this.games[gameId];
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
let p1Username = 'Игрок', p1CharName = 'Неизвестный';
const ownerId = game.ownerIdentifier;
if (p1Info && p1Info.socket && p1Info.socket.userData) { // Check for userData
p1Username = p1Info.socket.userData.username || `User#${String(p1Info.identifier).substring(0,4)}`;
const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey);
p1CharName = charData?.name || p1Info.chosenCharacterKey || 'Не выбран';
} else if (ownerId){
// console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is pending, p1Info not found or no socket/userData. Using owner info. Owner: ${ownerId}`);
// Try to find owner's socket if they are still connected to the server (even if not in this game's player list actively)
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
const ownerCharKey = game.playerCharacterKey; // This should be the char key of the first player/owner
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
}
// console.log(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} - Owner: ${ownerId}, P1 Username: ${p1Username}, Char: ${p1CharName}`);
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
}
return null;
})
.filter(info => info !== null);
}
broadcastAvailablePvPGames() {
const list = this.getAvailablePvPGamesListForClient();
// console.log(`[GameManager.broadcastAvailablePvPGames] Broadcasting list of ${list.length} games.`);
this.io.emit('availablePvPGamesList', list);
}
handleRequestGameState(socket, identifier) {
const gameIdFromMap = this.userIdentifierToGameId[identifier];
console.log(`[GameManager.handleRequestGameState] User: ${identifier} (New Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`);
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
if (game) {
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found for user ${identifier}. Game mode: ${game.mode}, Active players in instance: ${game.playerCount}, Total player entries: ${Object.keys(game.players).length}`);
const playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier);
console.log(`[GameManager.handleRequestGameState] PlayerInfo for user ${identifier} in game ${gameIdFromMap}: ${playerInfoInGameInstance ? `Role: ${playerInfoInGameInstance.id}, OldSocketInGame: ${playerInfoInGameInstance.socket?.id}, TempDisco: ${playerInfoInGameInstance.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
if (playerInfoInGameInstance) {
if (game.gameState?.isGameOver) {
console.warn(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} for user ${identifier} IS ALREADY OVER. Emitting 'gameNotFound'. Cleanup should handle map.`);
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
// _cleanupGame is responsible for clearing userIdentifierToGameId when a game ends.
// If the game is over, but the user is still mapped, _cleanupGame might not have run or completed fully.
// We don't call _cleanupGame here directly, as it might be called by the game ending logic itself.
return;
}
console.log(`[GameManager.handleRequestGameState] Restoring game ${gameIdFromMap} for user ${identifier}. NewSocket: ${socket.id}. OldSocketInGame: ${playerInfoInGameInstance.socket?.id}. Player role: ${playerInfoInGameInstance.id}`);
if (typeof game.handlePlayerReconnected === 'function') {
const reconnected = game.handlePlayerReconnected(playerInfoInGameInstance.id, socket);
console.log(`[GameManager.handleRequestGameState] Called game.handlePlayerReconnected for role ${playerInfoInGameInstance.id}. Result: ${reconnected}`);
if (!reconnected && game.gameState && !game.gameState.isGameOver) { // if reconnected failed but game is active
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected returned false for user ${identifier}. This might indicate an issue.`);
// It could be that the player wasn't marked as disconnected, or an error occurred.
// The client might still expect game state.
} else if (!reconnected && game.gameState?.isGameOver) {
// If reconnect failed AND game is over, ensure client gets gameNotFound.
socket.emit('gameNotFound', { message: 'Не удалось восстановить сессию: игра уже завершена.' });
return;
}
} else {
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} is missing handlePlayerReconnected method! Attempting fallback socket update.`);
const oldSocketId = playerInfoInGameInstance.socket?.id;
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
delete game.players[oldSocketId];
}
playerInfoInGameInstance.socket = socket;
game.players[socket.id] = playerInfoInGameInstance;
if (!game.playerSockets[playerInfoInGameInstance.id] || game.playerSockets[playerInfoInGameInstance.id].id !== socket.id) {
game.playerSockets[playerInfoInGameInstance.id] = socket;
}
}
// If handlePlayerReconnected returned false but game is not over, we might still need to send state.
// If game became game over inside handlePlayerReconnected, it should have emitted gameOver.
// Re-check game over state as handlePlayerReconnected might have changed it (e.g. if opponent didn't reconnect and game ended)
if (game.gameState?.isGameOver) {
console.warn(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} became game over during reconnect logic for ${identifier}. Emitting 'gameNotFound'.`);
socket.emit('gameNotFound', { message: 'Игра завершилась во время попытки переподключения.' });
return;
}
socket.join(game.id);
console.log(`[GameManager.handleRequestGameState] New socket ${socket.id} for user ${identifier} joined room ${game.id}.`);
const pCharKey = playerInfoInGameInstance.chosenCharacterKey;
const pData = dataUtils.getCharacterData(pCharKey);
const opponentRole = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const oCharKeyFromGameState = game.gameState?.[opponentRole]?.characterKey;
const oCharKeyFromInstance = playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey;
const oCharKey = oCharKeyFromGameState || oCharKeyFromInstance;
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
console.log(`[GameManager.handleRequestGameState] User's charKey: ${pCharKey}. Opponent's role: ${opponentRole}, charKey: ${oCharKey || 'None (pending/AI placeholder)'}.`);
if (pData && (oData || (game.mode === 'pvp' && game.playerCount === 1 && !oCharKey) || game.mode === 'ai') && game.gameState) {
const gameStateToSend = game.gameState;
const logBufferToSend = game.consumeLogBuffer();
console.log(`[GameManager.handleRequestGameState] Emitting 'gameStarted' (for restore) to ${identifier} for game ${game.id}. Game state isGameOver: ${gameStateToSend.isGameOver}. Log entries: ${logBufferToSend.length}`);
socket.emit('gameStarted', {
gameId: game.id,
yourPlayerId: playerInfoInGameInstance.id,
initialGameState: gameStateToSend,
playerBaseStats: pData.baseStats,
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
playerAbilities: pData.abilities,
opponentAbilities: oData?.abilities || [],
log: logBufferToSend,
clientConfig: { ...GAME_CONFIG }
});
if (game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier && !game.gameState.isGameOver) {
console.log(`[GameManager.handleRequestGameState] PvP game ${game.id} is still pending for owner ${identifier}. Emitting 'waitingForOpponent'.`);
socket.emit('waitingForOpponent');
}
if (!game.gameState.isGameOver && typeof game.turnTimer?.start === 'function' && !game.isGameEffectivelyPaused()) {
const isAiTurnForTimer = game.mode === 'ai' && !game.gameState.isPlayerTurn && game.gameState.opponent?.characterKey !== null;
console.log(`[GameManager.handleRequestGameState] Restarting turn timer for game ${game.id}. isPlayerTurn: ${game.gameState.isPlayerTurn}, isAiTurnForTimer: ${isAiTurnForTimer}`);
game.turnTimer.start(game.gameState.isPlayerTurn, isAiTurnForTimer);
} else if (game.gameState.isGameOver) {
console.log(`[GameManager.handleRequestGameState] Game ${game.id} is already over, no timer restart.`);
} else if (game.isGameEffectivelyPaused()){
console.log(`[GameManager.handleRequestGameState] Game ${game.id} is effectively paused, turn timer not started.`);
}
} else {
console.error(`[GameManager.handleRequestGameState] Data load failed for game ${game.id} / user ${identifier} on reconnect. pData: ${!!pData}, oData: ${!!oData} (oCharKey: ${oCharKey}), gameState: ${!!game.gameState}`);
this._handleGameRecoveryError(socket, game.id, identifier, 'data_load_fail_reconnect_manager');
}
} else {
console.error(`[GameManager.handleRequestGameState] User ${identifier} was mapped to game ${gameIdFromMap}, but NOT found in game.players. This indicates a serious state inconsistency.`);
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_instance_reconnect_manager');
}
} else {
console.log(`[GameManager.handleRequestGameState] No active game session found for user ${identifier} (GameID from map was ${gameIdFromMap || 'undefined'}). Emitting 'gameNotFound'.`);
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
// Ensure map is clear if it's somehow stale
if (this.userIdentifierToGameId[identifier]) {
console.warn(`[GameManager.handleRequestGameState] Clearing stale map entry for ${identifier} which pointed to ${this.userIdentifierToGameId[identifier]} but game not found.`);
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) {
// Attempt to cleanup the problematic game
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
} else if (this.userIdentifierToGameId[identifier]) {
// If gameId was null, but user was still mapped, cleanup the mapped game
const problematicGameId = this.userIdentifierToGameId[identifier];
console.warn(`[GameManager._handleGameRecoveryError] GameId was null/undefined for user ${identifier}, but they were in map to game ${problematicGameId}. Attempting cleanup of ${problematicGameId}.`);
this._cleanupGame(problematicGameId, `recovery_error_null_gameid_for_${identifier}_reason_${reasonCode}`);
}
// This will also clear userIdentifierToGameId[identifier] if _cleanupGame didn't.
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
}
}
module.exports = GameManager;