479 lines
33 KiB
JavaScript
479 lines
33 KiB
JavaScript
// /server_modules/gameManager.js
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const GameInstance = require('./gameInstance');
|
||
const gameData = require('./data');
|
||
const GAME_CONFIG = require('./config');
|
||
|
||
class GameManager {
|
||
constructor(io) {
|
||
this.io = io;
|
||
this.games = {}; // { gameId: GameInstance }
|
||
this.userIdentifierToGameId = {}; // { userId|socketId: gameId }
|
||
this.pendingPvPGames = []; // [gameId]
|
||
}
|
||
|
||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||
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 oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) {
|
||
console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);
|
||
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game');
|
||
}
|
||
} else {
|
||
if (this.userIdentifierToGameId[identifier] !== excludeGameId) {
|
||
console.warn(`[GameManager] Удаление потенциально некорректной ссылки userIdentifierToGameId[${identifier}] на игру ${oldPendingGameId}.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на создание.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
|
||
const gameId = uuidv4();
|
||
const game = new GameInstance(gameId, this.io, mode, this);
|
||
game.ownerIdentifier = identifier;
|
||
this.games[gameId] = game;
|
||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||
|
||
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameId;
|
||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (сокет: ${socket.id}, выбран: ${charKeyForInstance})`);
|
||
const assignedPlayerId = game.players[socket.id]?.id;
|
||
if (!assignedPlayerId) {
|
||
this._cleanupGame(gameId, 'player_add_failed');
|
||
console.error(`[GameManager] Ошибка при создании игры ${gameId}: Не удалось назначить ID игрока сокету ${socket.id} (идентификатор ${identifier}).`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры.' });
|
||
return;
|
||
}
|
||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||
|
||
if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) {
|
||
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) {
|
||
game.startGame();
|
||
} else {
|
||
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||
this._cleanupGame(gameId, 'initialization_failed');
|
||
}
|
||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else if (mode === 'pvp' && game.playerCount === 1) {
|
||
if (!this.pendingPvPGames.includes(gameId)) {
|
||
this.pendingPvPGames.push(gameId);
|
||
}
|
||
game.initializeGame(); // Частичная инициализация
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
this._cleanupGame(gameId, 'player_add_failed');
|
||
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}. Игра удалена.`);
|
||
}
|
||
}
|
||
|
||
joinGame(socket, gameId, identifier) {
|
||
const game = this.games[gameId];
|
||
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; }
|
||
if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]] && this.userIdentifierToGameId[identifier] !== gameId) {
|
||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на присоединение.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return; }
|
||
|
||
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
||
|
||
if (game.addPlayer(socket, null, identifier)) {
|
||
this.userIdentifierToGameId[identifier] = gameId;
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) присоединился к PvP игре ${gameId}`);
|
||
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) {
|
||
game.startGame();
|
||
} else {
|
||
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||
this._cleanupGame(gameId, 'initialization_failed');
|
||
}
|
||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}.`);
|
||
}
|
||
}
|
||
|
||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на поиск.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||
this.handleRequestGameState(socket, identifier);
|
||
return;
|
||
}
|
||
|
||
let gameIdToJoin = null;
|
||
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
|
||
|
||
for (const id of this.pendingPvPGames) {
|
||
const pendingGame = this.games[id];
|
||
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
|
||
const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
||
gameIdToJoin = id;
|
||
break;
|
||
}
|
||
if (!gameIdToJoin) gameIdToJoin = id;
|
||
}
|
||
}
|
||
|
||
if (gameIdToJoin) {
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) нашел игру ${gameIdToJoin} и присоединяется.`);
|
||
this.joinGame(socket, gameIdToJoin, identifier);
|
||
} else {
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) не нашел свободных игр. Создает новую.`);
|
||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
|
||
socket.emit('noPendingGamesFound', {
|
||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||
gameId: this.userIdentifierToGameId[identifier],
|
||
yourPlayerId: GAME_CONFIG.PLAYER_ID
|
||
});
|
||
}
|
||
}
|
||
|
||
handlePlayerAction(identifier, actionData) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
const game = this.games[gameId];
|
||
if (game && game.players) {
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
const currentSocketId = playerInfo?.socket?.id;
|
||
if (playerInfo && currentSocketId) {
|
||
const actualSocket = this.io.sockets.sockets.get(currentSocketId);
|
||
if (actualSocket && actualSocket.connected) {
|
||
game.processPlayerAction(currentSocketId, actionData);
|
||
} else {
|
||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его текущий сокет (${currentSocketId}) не найден или отключен.`);
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}) для игры ${gameId}, но его запись не найдена в game.players.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
const playerSocket = this.io.sockets.sockets.get(identifier) || playerInfo?.socket;
|
||
if (playerSocket) {
|
||
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его игра (ID: ${gameId}) не найдена в GameManager.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
const playerSocket = this.io.sockets.sockets.get(identifier);
|
||
if (playerSocket) {
|
||
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||
}
|
||
}
|
||
}
|
||
|
||
handleDisconnect(socketId, identifier) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
const game = this.games[gameId];
|
||
|
||
if (game && game.players) {
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
if (playerInfo) {
|
||
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socketId}) отключился. В игре ${gameId}.`);
|
||
const disconnectedPlayerRole = playerInfo.id;
|
||
const disconnectedCharacterKey = playerInfo.chosenCharacterKey;
|
||
|
||
game.removePlayer(socketId); // Удаляем именно этот сокет
|
||
|
||
if (game.playerCount === 0) {
|
||
console.log(`[GameManager] Игра ${gameId} пуста после дисконнекта ${socketId} (идентификатор ${identifier}). Удаляем.`);
|
||
this._cleanupGame(gameId, 'player_count_zero_on_disconnect');
|
||
} else if (game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
||
// Если это PvP игра и остался 1 игрок, и игра НЕ была завершена дисконнектом этого игрока
|
||
// (т.е. другой игрок еще в игре)
|
||
// Тогда игра переходит в состояние ожидания.
|
||
const remainingPlayerInfo = Object.values(game.players)[0]; // Единственный оставшийся игрок
|
||
if (remainingPlayerInfo) {
|
||
// Проверяем, что это не тот же игрок, что и отключившийся
|
||
if (remainingPlayerInfo.identifier !== identifier) {
|
||
game.endGameDueToDisconnect(socketId, disconnectedPlayerRole, disconnectedCharacterKey);
|
||
// _cleanupGame будет вызван из endGameDueToDisconnect
|
||
} else {
|
||
// Отключился единственный оставшийся игрок в ожидающей игре.
|
||
// _cleanupGame должен быть вызван.
|
||
console.log(`[GameManager] Отключился единственный игрок ${identifier} из ожидающей PvP игры ${gameId}. Удаляем игру.`);
|
||
this._cleanupGame(gameId, 'last_player_disconnected_from_pending');
|
||
}
|
||
} else {
|
||
// Оставшегося игрока нет, хотя playerCount > 0 - это ошибка, очищаем.
|
||
console.error(`[GameManager] Ошибка: playerCount > 0 в игре ${gameId}, но не найден оставшийся игрок после дисконнекта ${identifier}. Очищаем.`);
|
||
this._cleanupGame(gameId, 'error_no_remaining_player');
|
||
}
|
||
} else if (game.gameState && !game.gameState.isGameOver) {
|
||
// Если игра была активна (не ожидала) и еще не была завершена,
|
||
// дисконнект одного из игроков завершает игру.
|
||
game.endGameDueToDisconnect(socketId, disconnectedPlayerRole, disconnectedCharacterKey);
|
||
// _cleanupGame будет вызван из endGameDueToDisconnect
|
||
} else if (game.gameState?.isGameOver) {
|
||
// Если игра уже была завершена до этого дисконнекта, просто удаляем ссылку на игру для отключившегося.
|
||
console.log(`[GameManager] Игрок ${identifier} отключился из уже завершенной игры ${gameId}. Удаляем ссылку.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
// _cleanupGame уже был вызван при завершении игры.
|
||
} else {
|
||
// Другие случаи (например, AI игра, где игрок остался, или ошибка)
|
||
console.log(`[GameManager] Игрок ${identifier} отключился из активной игры ${gameId} (mode: ${game.mode}, players: ${game.playerCount}). Удаляем ссылку.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] Игрок с идентификатором ${identifier} (сокет: ${socketId}) не найден в game.players для игры ${gameId}.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
} else {
|
||
console.log(`[GameManager] Отключился сокет ${socketId} (идентификатор ${identifier}). Игровая сессия по этому идентификатору не найдена.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
}
|
||
}
|
||
|
||
_cleanupGame(gameId, reason = 'unknown_reason') {
|
||
const game = this.games[gameId];
|
||
if (!game) {
|
||
console.warn(`[GameManager] _cleanupGame called for unknown game ID: ${gameId}`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`[GameManager] Cleaning up game ${gameId} (Mode: ${game.mode}, Reason: ${reason})...`);
|
||
|
||
// Очищаем таймеры, если они были активны
|
||
if (typeof game.clearTurnTimer === 'function') {
|
||
game.clearTurnTimer();
|
||
}
|
||
|
||
Object.values(game.players).forEach(playerInfo => {
|
||
if (playerInfo && playerInfo.identifier && this.userIdentifierToGameId[playerInfo.identifier] === gameId) {
|
||
delete this.userIdentifierToGameId[playerInfo.identifier];
|
||
console.log(`[GameManager] Removed userIdentifierToGameId for ${playerInfo.identifier}.`);
|
||
}
|
||
});
|
||
|
||
const pendingIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (pendingIndex > -1) {
|
||
this.pendingPvPGames.splice(pendingIndex, 1);
|
||
console.log(`[GameManager] Removed game ${gameId} from pendingPvPGames.`);
|
||
}
|
||
|
||
delete this.games[gameId];
|
||
console.log(`[GameManager] Deleted GameInstance for game ${gameId}.`);
|
||
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) {
|
||
let firstPlayerUsername = 'Игрок';
|
||
let firstPlayerCharacterName = '';
|
||
const firstPlayerInfo = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
|
||
if (firstPlayerInfo) {
|
||
if (firstPlayerInfo.socket?.userData?.username) {
|
||
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
|
||
} else {
|
||
firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0, 6)}`;
|
||
}
|
||
const charKey = firstPlayerInfo.chosenCharacterKey;
|
||
if (charKey) {
|
||
const charBaseStats = this._getCharacterBaseData(charKey);
|
||
if (charBaseStats && charBaseStats.name) {
|
||
firstPlayerCharacterName = charBaseStats.name;
|
||
} else {
|
||
firstPlayerCharacterName = charKey;
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo (Player 1) не найдена для ожидающей игры ${gameId}.`);
|
||
firstPlayerUsername = 'Неизвестный игрок';
|
||
}
|
||
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
|
||
if (firstPlayerCharacterName) statusString += ` за ${firstPlayerCharacterName}`;
|
||
statusString += `)`;
|
||
return { id: gameId, status: statusString };
|
||
}
|
||
if (game && !this.pendingPvPGames.includes(gameId)) { /* Game not pending */ }
|
||
else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) { /* Game over or not initialized */ }
|
||
else if (game && game.playerCount === 2) { /* Game full */ }
|
||
else if (game && game.playerCount === 0) {
|
||
console.warn(`[GameManager] getAvailablePvPGamesList: Найдена пустая игра ${gameId} в games. Удаляем.`);
|
||
delete this.games[gameId];
|
||
}
|
||
return null;
|
||
})
|
||
.filter(info => info !== null);
|
||
}
|
||
|
||
broadcastAvailablePvPGames() {
|
||
const availableGames = this.getAvailablePvPGamesListForClient();
|
||
this.io.emit('availablePvPGamesList', availableGames);
|
||
console.log(`[GameManager] Обновлен список доступных PvP игр. Всего: ${availableGames.length}`);
|
||
}
|
||
|
||
getActiveGamesList() {
|
||
return Object.values(this.games).map(game => {
|
||
let playerSlotCharName = game.gameState?.player?.name || (game.playerCharacterKey ? this._getCharacterBaseData(game.playerCharacterKey)?.name : 'N/A (ожидание)');
|
||
let opponentSlotCharName = game.gameState?.opponent?.name || (game.opponentCharacterKey ? this._getCharacterBaseData(game.opponentCharacterKey)?.name : 'N/A (ожидание)');
|
||
const playerInSlot1 = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
const playerInSlot2 = Object.values(game.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||
|
||
if (!playerInSlot1) playerSlotCharName = 'Пусто';
|
||
if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...';
|
||
if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)';
|
||
|
||
return {
|
||
id: game.id.substring(0, 8),
|
||
mode: game.mode,
|
||
playerCount: game.playerCount,
|
||
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A (Не инициализирована)',
|
||
playerSlot: playerSlotCharName,
|
||
opponentSlot: opponentSlotCharName,
|
||
ownerIdentifier: game.ownerIdentifier || 'N/A',
|
||
pending: this.pendingPvPGames.includes(game.id),
|
||
turn: game.gameState ? `Ход ${game.gameState.turnNumber}, ${game.gameState.isPlayerTurn ? (playerInSlot1?.identifier || 'Player Slot') : (playerInSlot2?.identifier || 'Opponent Slot')}` : 'N/A'
|
||
};
|
||
});
|
||
}
|
||
|
||
handleRequestGameState(socket, identifier) {
|
||
const gameId = this.userIdentifierToGameId[identifier];
|
||
let game = gameId ? this.games[gameId] : null;
|
||
|
||
if (game && game.players) {
|
||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||
if (playerInfo) {
|
||
if (game.gameState?.isGameOver) {
|
||
console.log(`[GameManager] Reconnected user ${identifier} to game ${gameId} which is already over. Sending gameNotFound.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||
return;
|
||
}
|
||
|
||
console.log(`[GameManager] Found game ${gameId} for identifier ${identifier} (role ${playerInfo.id}). Reconnecting socket ${socket.id}.`);
|
||
const oldSocketId = playerInfo.socket?.id;
|
||
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
||
console.log(`[GameManager] Updating socket ID for player ${identifier} from ${oldSocketId} to ${socket.id} in game ${gameId}.`);
|
||
delete game.players[oldSocketId];
|
||
if (game.playerSockets[playerInfo.id]?.id === oldSocketId) {
|
||
delete game.playerSockets[playerInfo.id];
|
||
}
|
||
}
|
||
game.players[socket.id] = playerInfo;
|
||
game.players[socket.id].socket = socket;
|
||
game.playerSockets[playerInfo.id] = socket;
|
||
socket.join(game.id);
|
||
|
||
const playerCharDataForClient = this._getCharacterData(playerInfo.chosenCharacterKey);
|
||
const opponentActualSlotId = playerInfo.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
let opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null;
|
||
if (!opponentCharacterKeyForClient) {
|
||
opponentCharacterKeyForClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey;
|
||
}
|
||
const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient);
|
||
|
||
if (playerCharDataForClient && opponentCharDataForClient && game.gameState) {
|
||
const isGameReadyForPlay = (game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2);
|
||
const isOpponentDefinedInState = game.gameState.opponent?.characterKey && game.gameState.opponent?.name !== 'Ожидание игрока...';
|
||
|
||
socket.emit('gameState', {
|
||
gameId: game.id, yourPlayerId: playerInfo.id, gameState: game.gameState,
|
||
playerBaseStats: playerCharDataForClient.baseStats, opponentBaseStats: opponentCharDataForClient.baseStats,
|
||
playerAbilities: playerCharDataForClient.abilities, opponentAbilities: opponentCharDataForClient.abilities,
|
||
log: game.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||
});
|
||
console.log(`[GameManager] Sent gameState to socket ${socket.id} (identifier: ${identifier}) for game ${game.id}.`);
|
||
|
||
if (!game.gameState.isGameOver && isGameReadyForPlay && !isOpponentDefinedInState) {
|
||
console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect. Initializing/Starting.`);
|
||
const isInitialized = game.initializeGame();
|
||
if (isInitialized) {
|
||
game.startGame();
|
||
} else {
|
||
console.error(`[GameManager] Failed to initialize game ${game.id} on reconnect. Cannot start.`);
|
||
this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения.' });
|
||
this._cleanupGame(gameId, 'reconnect_initialization_failed');
|
||
}
|
||
} else if (!isGameReadyForPlay) {
|
||
console.log(`[GameManager] Reconnected user ${identifier} to pending game ${gameId}. Sending gameState and waiting status.`);
|
||
socket.emit('waitingForOpponent');
|
||
} else if (game.gameState.isGameOver) { // Повторная проверка, т.к. startGame мог завершить игру
|
||
console.log(`[GameManager] Reconnected to game ${gameId} which is now over (after re-init). Sending gameNotFound.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия завершилась во время переподключения.' });
|
||
} else {
|
||
console.log(`[GameManager] Reconnected user ${identifier} to active game ${gameId}. gameState sent.`);
|
||
// Важно: если игра активна, нужно отправить и текущее состояние таймера.
|
||
// Это можно сделать, вызвав game.startTurnTimer() (он отправит update),
|
||
// но только если это ход этого игрока и игра не AI (или ход игрока в AI).
|
||
// Или добавить отдельный метод в GameInstance для отправки текущего состояния таймера.
|
||
if (typeof game.startTurnTimer === 'function') { // Проверяем, что метод существует
|
||
// Перезапуск таймера здесь может быть некорректным, если ход не этого игрока
|
||
// Лучше, чтобы gameInstance сам отправлял 'turnTimerUpdate' при gameState
|
||
// Либо добавить специальный метод в gameInstance для отправки текущего значения таймера
|
||
// Пока оставим так, startTurnTimer сам проверит, чей ход.
|
||
game.startTurnTimer();
|
||
}
|
||
}
|
||
} else {
|
||
console.error(`[GameManager] Failed to send gameState to ${socket.id} (identifier ${identifier}) for game ${gameId}: missing character data or gameState.`);
|
||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||
this._cleanupGame(gameId, 'reconnect_send_failed');
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия в некорректном состоянии и была завершена.' });
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] Found game ${gameId} by identifier ${identifier}, but player with this identifier not found in game.players.`);
|
||
delete this.userIdentifierToGameId[identifier];
|
||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена. Возможно, идентификатор пользователя некорректен.' });
|
||
}
|
||
} else {
|
||
console.log(`[GameManager] No active or pending game found for identifier ${identifier}.`);
|
||
socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' });
|
||
}
|
||
}
|
||
|
||
_getCharacterData(key) {
|
||
if (!key) { console.warn("GameManager::_getCharacterData called with null/undefined key."); return null; }
|
||
switch (key) {
|
||
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
|
||
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities };
|
||
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities };
|
||
default: console.error(`GameManager::_getCharacterData: Unknown character key "${key}"`); return null;
|
||
}
|
||
}
|
||
_getCharacterBaseData(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.baseStats : null;
|
||
}
|
||
_getCharacterAbilities(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.abilities : null;
|
||
}
|
||
}
|
||
|
||
module.exports = GameManager; |