bc/server_modules/gameManager.js
2025-05-13 04:14:01 +00:00

275 lines
16 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_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;