275 lines
16 KiB
JavaScript
275 lines
16 KiB
JavaScript
// /server_modules/gameManager.js
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const GameInstance = require('./gameInstance');
|
||
const gameData = require('./data');
|
||
|
||
class GameManager {
|
||
constructor(io) {
|
||
this.io = io;
|
||
this.games = {}; // { gameId: GameInstance }
|
||
this.socketToGame = {}; // { socket.id: gameId }
|
||
this.pendingPvPGames = []; // [gameId]
|
||
this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId }
|
||
}
|
||
|
||
/**
|
||
* Удаляет предыдущие ожидающие PvP игры, созданные этим же пользователем/сокетом.
|
||
* @param {string} currentSocketId - ID текущего сокета игрока.
|
||
* @param {number|string} [identifier] - userId игрока или socketId.
|
||
* @param {string} [excludeGameId] - ID игры, которую НЕ нужно удалять.
|
||
*/
|
||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||
const keyToUse = identifier || currentSocketId;
|
||
const oldPendingGameId = this.userToPendingGame[keyToUse];
|
||
|
||
if (oldPendingGameId && oldPendingGameId !== excludeGameId) {
|
||
const gameToRemove = this.games[oldPendingGameId];
|
||
if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||
const playersInOldGame = Object.values(gameToRemove.players);
|
||
// Убеждаемся, что единственный игрок в старой игре - это действительно тот, кто сейчас создает/присоединяется
|
||
// Либо по ID сокета, либо по userId, если он был владельцем
|
||
const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId;
|
||
const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier;
|
||
|
||
if (isOwnerBySocket || isOwnerByUserId) {
|
||
console.log(`[GameManager] Пользователь ${keyToUse} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);
|
||
|
||
delete this.games[oldPendingGameId];
|
||
const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId);
|
||
if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1);
|
||
|
||
// Удаляем привязки для старого сокета, если он там был
|
||
if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) {
|
||
delete this.socketToGame[playersInOldGame[0].socket.id];
|
||
}
|
||
delete this.userToPendingGame[keyToUse]; // Удаляем по ключу, который использовали для поиска
|
||
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else if (oldPendingGameId === excludeGameId) {
|
||
// Это та же игра, ничего не делаем
|
||
} else {
|
||
// Запись в userToPendingGame устарела или не соответствует условиям, чистим
|
||
delete this.userToPendingGame[keyToUse];
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) {
|
||
const identifier = userId || socket.id;
|
||
this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед созданием новой
|
||
|
||
const gameId = uuidv4();
|
||
const game = new GameInstance(gameId, this.io, mode);
|
||
if (userId) game.ownerUserId = userId; // Устанавливаем владельца игры
|
||
this.games[gameId] = game;
|
||
|
||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||
|
||
if (game.addPlayer(socket, charKeyForInstance)) { // addPlayer теперь сам установит userId в game.ownerUserId если это первый игрок
|
||
this.socketToGame[socket.id] = gameId; // Устанавливаем привязку после успешного добавления
|
||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`);
|
||
|
||
const assignedPlayerId = game.players[socket.id]?.id;
|
||
if (!assignedPlayerId) {
|
||
delete this.games[gameId]; if(this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
|
||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (ID игрока).' }); return;
|
||
}
|
||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||
|
||
if (mode === 'pvp') {
|
||
if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId);
|
||
this.userToPendingGame[identifier] = gameId;
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
delete this.games[gameId]; // game.addPlayer вернул false, чистим
|
||
// socketToGame не должен был быть установлен, если addPlayer вернул false
|
||
if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
|
||
socket.emit('gameError', { message: 'Не удалось создать игру или добавить игрока.' });
|
||
}
|
||
}
|
||
|
||
joinGame(socket, gameId, userId = null) {
|
||
const identifier = userId || socket.id;
|
||
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.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре (попытка двойного присоединения).' }); return;} // Доп. проверка
|
||
|
||
// Удаляем предыдущие ожидающие игры этого пользователя
|
||
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
||
|
||
if (game.addPlayer(socket)) {
|
||
this.socketToGame[socket.id] = gameId;
|
||
// console.log(`[GameManager] Игрок ${socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`);
|
||
|
||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||
|
||
// Очищаем запись о создателе из userToPendingGame, так как игра началась
|
||
if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) {
|
||
delete this.userToPendingGame[game.ownerUserId];
|
||
} else { // Если ownerUserId не был или не совпал, пробуем по socketId первого игрока
|
||
const firstPlayerSocketId = Object.keys(game.players).find(sId => game.players[sId].id === GAME_CONFIG.PLAYER_ID && game.players[sId].socket.id !== socket.id);
|
||
if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) {
|
||
delete this.userToPendingGame[firstPlayerSocketId];
|
||
}
|
||
}
|
||
this.broadcastAvailablePvPGames();
|
||
} else {
|
||
socket.emit('gameError', { message: 'Не удалось присоединиться к игре (внутренняя ошибка).' });
|
||
}
|
||
}
|
||
|
||
findAndJoinRandomPvPGame(socket, chosenCharacterKey = 'elena', userId = null) {
|
||
const identifier = userId || socket.id;
|
||
this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед поиском/созданием
|
||
|
||
let gameIdToJoin = null;
|
||
const preferredOpponentKey = chosenCharacterKey === 'elena' ? 'almagest' : 'elena';
|
||
|
||
for (const id of this.pendingPvPGames) {
|
||
const pendingGame = this.games[id];
|
||
if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') {
|
||
const firstPlayerInfo = Object.values(pendingGame.players)[0];
|
||
const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id);
|
||
if (isMyOwnGame) continue;
|
||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
||
gameIdToJoin = id; break;
|
||
}
|
||
}
|
||
}
|
||
if (!gameIdToJoin && this.pendingPvPGames.length > 0) {
|
||
for (const id of this.pendingPvPGames) {
|
||
const pendingGame = this.games[id];
|
||
if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') {
|
||
const firstPlayerInfo = Object.values(pendingGame.players)[0];
|
||
const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id);
|
||
if (isMyOwnGame) continue;
|
||
gameIdToJoin = id; break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (gameIdToJoin) {
|
||
this.joinGame(socket, gameIdToJoin, userId);
|
||
} else {
|
||
this.createGame(socket, 'pvp', chosenCharacterKey, userId);
|
||
socket.emit('noPendingGamesFound', {
|
||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||
});
|
||
}
|
||
}
|
||
|
||
handlePlayerAction(socketId, actionData) {
|
||
const gameIdFromSocket = this.socketToGame[socketId];
|
||
const game = this.games[gameIdFromSocket];
|
||
if (game) {
|
||
game.processPlayerAction(socketId, actionData);
|
||
} else {
|
||
const playerSocket = this.io.sockets.sockets.get(socketId);
|
||
if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна.' });
|
||
}
|
||
}
|
||
|
||
requestRestart(socketId, gameId) {
|
||
const game = this.games[gameId];
|
||
if (game && game.players[socketId]) {
|
||
game.handleVoteRestart(socketId);
|
||
} else {
|
||
const playerSocket = this.io.sockets.sockets.get(socketId);
|
||
if (playerSocket) playerSocket.emit('gameError', { message: 'Не удалось перезапустить: сессия не найдена.' });
|
||
}
|
||
}
|
||
|
||
handleDisconnect(socketId, userId = null) {
|
||
const identifier = userId || socketId;
|
||
const gameId = this.socketToGame[socketId];
|
||
|
||
if (gameId && this.games[gameId]) {
|
||
const game = this.games[gameId];
|
||
console.log(`[GameManager] Игрок ${socketId} (userId: ${userId}) отключился от игры ${gameId}.`);
|
||
game.removePlayer(socketId);
|
||
|
||
if (game.playerCount === 0) {
|
||
console.log(`[GameManager] Игра ${gameId} пуста и будет удалена (после дисконнекта).`);
|
||
delete this.games[gameId];
|
||
const gameIndexPending = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1);
|
||
// Удаляем из userToPendingGame, если игра была там по любому ключу
|
||
for (const key in this.userToPendingGame) {
|
||
if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key];
|
||
}
|
||
this.broadcastAvailablePvPGames();
|
||
} else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||
if (!this.pendingPvPGames.includes(gameId)) {
|
||
this.pendingPvPGames.push(gameId);
|
||
}
|
||
// Обновляем ownerUserId и userToPendingGame для оставшегося игрока
|
||
const remainingPlayerSocketId = Object.keys(game.players)[0];
|
||
const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket;
|
||
const remainingUserId = remainingPlayerSocket?.userData?.userId;
|
||
const newIdentifier = remainingUserId || remainingPlayerSocketId;
|
||
|
||
game.ownerUserId = remainingUserId; // Устанавливаем нового владельца (может быть null)
|
||
this.userToPendingGame[newIdentifier] = gameId; // Связываем нового владельца
|
||
|
||
// Удаляем старую привязку отключившегося, если она была и не совпадает с новой
|
||
if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) {
|
||
delete this.userToPendingGame[identifier];
|
||
}
|
||
console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else { // Если игрок не был в активной игре, но мог иметь ожидающую
|
||
this._removePreviousPendingGames(socketId, identifier);
|
||
}
|
||
delete this.socketToGame[socketId]; // Всегда удаляем эту связь
|
||
}
|
||
|
||
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 firstPlayerName = 'Игрок';
|
||
if (game.players && Object.keys(game.players).length > 0) {
|
||
const firstPlayerSocketId = Object.keys(game.players)[0];
|
||
const firstPlayerInfo = game.players[firstPlayerSocketId];
|
||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey) {
|
||
const charData = gameData[firstPlayerInfo.chosenCharacterKey + 'BaseStats'];
|
||
if (charData) firstPlayerName = charData.name;
|
||
}
|
||
}
|
||
return { id: gameId, status: `Ожидает 1 игрока (Создал: ${firstPlayerName})` };
|
||
}
|
||
return null;
|
||
})
|
||
.filter(info => info !== null);
|
||
}
|
||
|
||
broadcastAvailablePvPGames() {
|
||
this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient());
|
||
}
|
||
|
||
getActiveGamesList() {
|
||
return Object.values(this.games).map(game => {
|
||
let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey + 'BaseStats']?.name : 'N/A');
|
||
let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey + 'BaseStats']?.name : 'N/A');
|
||
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey) opponentSlotChar = 'Ожидание...';
|
||
|
||
return {
|
||
id: game.id, mode: game.mode, playerCount: game.playerCount,
|
||
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A',
|
||
playerSlot: playerSlotChar, opponentSlot: opponentSlotChar,
|
||
ownerUserId: game.ownerUserId || 'N/A'
|
||
};
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = GameManager; |