bc/server/game/GameManager.js
2025-05-18 10:50:38 +03:00

343 lines
21 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 = {};
this.userIdentifierToGameId = {};
this.pendingPvPGames = [];
console.log("[GameManager] Initialized.");
}
_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');
}
}
}
}
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
this._removePreviousPendingGames(socket.id, identifier);
if (this.userIdentifierToGameId[identifier] && this.games[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} (выбран: ${charKeyForInstance})`);
const assignedPlayerId = game.players[socket.id]?.id;
if (!assignedPlayerId) {
this._cleanupGame(gameId, 'player_add_failed_no_role');
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)) {
const isInitialized = game.initializeGame();
if (isInitialized) game.startGame();
else this._cleanupGame(gameId, 'initialization_failed_on_create');
if (game.mode === 'pvp' && game.playerCount === 2) { // Если PvP заполнилась
const idx = this.pendingPvPGames.indexOf(gameId);
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
this.broadcastAvailablePvPGames();
}
} else if (mode === 'pvp' && game.playerCount === 1) {
if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId);
game.initializeGame();
socket.emit('waitingForOpponent');
this.broadcastAvailablePvPGames();
}
} else {
this._cleanupGame(gameId, 'player_add_failed_instance');
}
}
joinGame(socket, gameId, identifier) { // identifier - это userId присоединяющегося
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 (game.ownerIdentifier === identifier) {
socket.emit('gameError', { message: 'Вы не можете присоединиться к игре, которую сами создали и ожидаете.' });
// Можно отправить состояние этой игры, если она действительно ожидает
this.handleRequestGameState(socket, identifier);
return;
}
// === КОНЕЦ ИЗМЕНЕНИЯ ===
if (this.userIdentifierToGameId[identifier] && this.userIdentifierToGameId[identifier] !== gameId) {
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
this.handleRequestGameState(socket, identifier);
return;
}
// Проверка на случай, если игрок пытается присоединиться к игре, где он уже есть (хотя ownerIdentifier проверка выше это частично покрывает для создателя)
const existingPlayerInThisGame = Object.values(game.players).find(p => p.identifier === identifier);
if (existingPlayerInThisGame) {
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' });
this.handleRequestGameState(socket, identifier); // Отправляем состояние игры
return;
}
this._removePreviousPendingGames(socket.id, identifier, gameId);
if (game.addPlayer(socket, null, identifier)) {
this.userIdentifierToGameId[identifier] = gameId;
console.log(`[GameManager] Игрок ${identifier} присоединился к PvP игре ${gameId}`);
if (game.mode === 'pvp' && game.playerCount === 2) {
const isInitialized = game.initializeGame();
if (isInitialized) game.startGame();
else this._cleanupGame(gameId, 'initialization_failed_on_join');
const idx = this.pendingPvPGames.indexOf(gameId);
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
this.broadcastAvailablePvPGames();
}
}
}
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
this._removePreviousPendingGames(socket.id, identifier);
if (this.userIdentifierToGameId[identifier] && this.games[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) {
this.joinGame(socket, gameIdToJoin, identifier);
} else {
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier);
// Сообщение о создании новой игры отправляется из createGame/initializeGame/startGame
}
}
handlePlayerAction(identifier, actionData) {
const gameId = this.userIdentifierToGameId[identifier];
const game = this.games[gameId];
if (game) {
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?.connected) game.processPlayerAction(currentSocketId, actionData);
else console.warn(`[GameManager] Игрок ${identifier}: действие, но сокет ${currentSocketId} отключен.`);
} else {
console.warn(`[GameManager] Игрок ${identifier}: действие для игры ${gameId}, но не найден в game.players.`);
delete this.userIdentifierToGameId[identifier];
const s = this.io.sockets.sockets.get(identifier) || playerInfo?.socket;
if (s) s.emit('gameNotFound', { message: 'Ваша игровая сессия потеряна (ошибка игрока).' });
}
} else {
console.warn(`[GameManager] Игрок ${identifier}: действие, но игра ${gameId} не найдена.`);
delete this.userIdentifierToGameId[identifier];
const s = this.io.sockets.sockets.get(identifier);
if (s) s.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена.' });
}
}
handleDisconnect(socketId, identifier) {
const gameId = this.userIdentifierToGameId[identifier];
const game = this.games[gameId];
if (game) {
// Ищем игрока по ИДЕНТИФИКАТОРУ, так как сокет мог уже обновиться при переподключении
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
if (playerInfo) {
// Проверяем, действительно ли отключается АКТУАЛЬНЫЙ сокет этого игрока
if (playerInfo.socket.id === socketId) {
console.log(`[GameManager] Актуальный сокет ${socketId} игрока ${identifier} отключился из игры ${gameId}.`);
const dPlayerRole = playerInfo.id;
const dCharKey = playerInfo.chosenCharacterKey;
game.removePlayer(socketId); // Удаляем именно этот сокет из игры
if (game.playerCount === 0) {
this._cleanupGame(gameId, 'all_players_disconnected');
} else if (game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
game.endGameDueToDisconnect(socketId, dPlayerRole, dCharKey);
} else if (game.mode === 'ai' && game.playerCount === 0 && game.gameState && !game.gameState.isGameOver) {
game.endGameDueToDisconnect(socketId, dPlayerRole, dCharKey); // Завершаем AI игру, если игрок ушел
}
// Если игра уже была isGameOver, _cleanupGame был вызван ранее.
// userIdentifierToGameId[identifier] для отключившегося игрока УДАЛЯЕТСЯ здесь,
// чтобы он мог начать новую игру или переподключиться.
delete this.userIdentifierToGameId[identifier];
} else {
// Отключился старый сокет (socketId), но у игрока (identifier) уже новый активный сокет.
// Ничего не делаем с игрой, так как игрок по-прежнему в ней с новым сокетом.
// Просто логируем, что старый сокет отвалился.
console.log(`[GameManager] Отключился старый сокет ${socketId} для игрока ${identifier}, который уже переподключился с сокетом ${playerInfo.socket.id} в игре ${gameId}.`);
// Связь userIdentifierToGameId[identifier] остается, так как он все еще в игре.
}
} else {
// Игрока с таким identifier нет в этой игре.
// Это может случиться, если игра была очищена до того, как пришло событие disconnect.
// console.log(`[GameManager] Отключившийся сокет ${socketId} (identifier: ${identifier}) не найден в активных игроках игры ${gameId} (возможно, игра уже очищена).`);
delete this.userIdentifierToGameId[identifier]; // На всякий случай.
}
} else {
// console.log(`[GameManager] Отключился сокет ${socketId} (identifier: ${identifier}). Активная игра не найдена по идентификатору.`);
delete this.userIdentifierToGameId[identifier];
}
}
_cleanupGame(gameId, reason = 'unknown') {
const game = this.games[gameId];
if (!game) return false;
console.log(`[GameManager] Очистка игры ${gameId} (Причина: ${reason}).`);
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
Object.values(game.players).forEach(pInfo => {
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
delete this.userIdentifierToGameId[pInfo.identifier];
}
});
if(game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId){
delete this.userIdentifierToGameId[game.ownerIdentifier];
}
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) this.pendingPvPGames.splice(pendingIdx, 1);
delete this.games[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) {
const p1Info = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
let p1Username = 'Игрок';
let p1CharName = '';
let ownerId = game.ownerIdentifier; // === ИЗМЕНЕНИЕ: Получаем ownerId ===
if (p1Info) {
p1Username = p1Info.socket?.userData?.username || `User#${String(p1Info.identifier).substring(0,4)}`;
const charData = dataUtils.getCharacterBaseStats(p1Info.chosenCharacterKey);
p1CharName = charData?.name || p1Info.chosenCharacterKey;
}
return {
id: gameId,
status: `Ожидает (Создал: ${p1Username} за ${p1CharName})`,
ownerIdentifier: ownerId // === ИЗМЕНЕНИЕ: Отправляем ownerIdentifier клиенту ===
};
}
return null;
}).filter(info => info !== null);
}
broadcastAvailablePvPGames() {
this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient());
}
handleRequestGameState(socket, identifier) {
const gameId = this.userIdentifierToGameId[identifier];
const game = gameId ? this.games[gameId] : null;
if (game) {
const playerInfoInGameInstance = Object.values(game.players).find(p => p.identifier === identifier);
if (playerInfoInGameInstance) {
if (game.gameState?.isGameOver) {
delete this.userIdentifierToGameId[identifier];
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
return;
}
console.log(`[GameManager] Восстановление игры ${gameId} для ${identifier}. Новый сокет ${socket.id}.`);
const oldSocketId = playerInfoInGameInstance.socket?.id; // Добавил ?. на случай если сокета нет
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
delete game.players[oldSocketId];
if(game.playerSockets[playerInfoInGameInstance.id]?.id === oldSocketId) {
delete game.playerSockets[playerInfoInGameInstance.id];
}
}
playerInfoInGameInstance.socket = socket;
game.players[socket.id] = playerInfoInGameInstance;
game.playerSockets[playerInfoInGameInstance.id] = socket;
socket.join(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 oCharKey = game.gameState?.[opponentRole]?.characterKey || (playerInfoInGameInstance.id === GAME_CONFIG.PLAYER_ID ? game.opponentCharacterKey : game.playerCharacterKey);
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; // oData может быть null, если оппонента нет
if (pData && (oData || !game.opponentCharacterKey) && game.gameState) {
socket.emit('gameStarted', {
gameId: game.id, yourPlayerId: playerInfoInGameInstance.id, initialGameState: game.gameState,
playerBaseStats: pData.baseStats,
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1}, // Заглушка если оппонента нет
playerAbilities: pData.abilities,
opponentAbilities: oData?.abilities || [],
log: game.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
});
if(game.mode === 'pvp' && game.playerCount === 1 && game.ownerIdentifier === identifier) socket.emit('waitingForOpponent');
if (!game.gameState.isGameOver && game.turnTimer?.start) {
game.turnTimer.start(game.gameState.isPlayerTurn, (game.mode === 'ai' && !game.gameState.isPlayerTurn));
}
} else {
this._handleGameRecoveryError(socket, gameId, identifier, 'data_load_fail_reconnect');
}
} else {
this._handleGameRecoveryError(socket, gameId, identifier, 'player_not_in_instance_reconnect');
}
} else {
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
}
}
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
console.error(`[GameManager] Ошибка восстановления игры ${gameId} для ${identifier} (причина: ${reasonCode}).`);
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
this._cleanupGame(gameId, `recovery_error_${reasonCode}`);
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
}
}
module.exports = GameManager;