343 lines
21 KiB
JavaScript
343 lines
21 KiB
JavaScript
// /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; |