311 lines
18 KiB
JavaScript
311 lines
18 KiB
JavaScript
// /server_modules/gameManager.js
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const GameInstance = require('./gameInstance');
|
||
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient
|
||
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
||
|
||
class GameManager {
|
||
constructor(io) {
|
||
this.io = io;
|
||
this.games = {}; // { gameId: GameInstance }
|
||
this.socketToGame = {}; // { socket.id: gameId }
|
||
this.pendingPvPGames = []; // [gameId] - ID игр, ожидающих второго игрока в PvP
|
||
this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId } - для отслеживания созданных ожидающих игр
|
||
}
|
||
|
||
_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);
|
||
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 {
|
||
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;
|
||
|
||
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали
|
||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||
|
||
if (game.addPlayer(socket, charKeyForInstance)) {
|
||
this.socketToGame[socket.id] = gameId;
|
||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.userData?.username || 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];
|
||
if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
|
||
// Сообщение об ошибке отправляется из game.addPlayer
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
|
||
if (game.addPlayer(socket)) {
|
||
this.socketToGame[socket.id] = gameId;
|
||
console.log(`[GameManager] Игрок ${socket.userData?.username || socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`);
|
||
|
||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||
|
||
if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) {
|
||
delete this.userToPendingGame[game.ownerUserId];
|
||
} else {
|
||
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 {
|
||
// Сообщение об ошибке отправляется из game.addPlayer
|
||
}
|
||
}
|
||
|
||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', userId = null) {
|
||
const identifier = userId || socket.id;
|
||
this._removePreviousPendingGames(socket.id, identifier);
|
||
|
||
let gameIdToJoin = null;
|
||
// Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору)
|
||
const preferredOpponentKey = chosenCharacterKeyForCreation === '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) {
|
||
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
|
||
this.joinGame(socket, gameIdToJoin, userId);
|
||
} else {
|
||
// Если свободных игр нет, создаем новую с выбранным персонажем
|
||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, userId);
|
||
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
|
||
socket.emit('noPendingGamesFound', {
|
||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||
gameId: this.userToPendingGame[identifier], // ID только что созданной игры
|
||
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
|
||
});
|
||
}
|
||
}
|
||
|
||
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: 'Ошибка: игровая сессия потеряна для этого действия.' });
|
||
}
|
||
}
|
||
|
||
handleDisconnect(socketId, userId = null) {
|
||
const identifier = userId || socketId;
|
||
const gameId = this.socketToGame[socketId];
|
||
|
||
if (gameId && this.games[gameId]) {
|
||
const game = this.games[gameId];
|
||
const playerInfo = game.players[socketId];
|
||
const username = playerInfo?.socket?.userData?.username || socketId;
|
||
console.log(`[GameManager] Игрок ${username} (socket: ${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);
|
||
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);
|
||
}
|
||
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;
|
||
this.userToPendingGame[newIdentifier] = gameId;
|
||
|
||
if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) {
|
||
delete this.userToPendingGame[identifier];
|
||
}
|
||
console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`);
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
} else {
|
||
const pendingGameIdToRemove = this.userToPendingGame[identifier];
|
||
if (pendingGameIdToRemove && this.games[pendingGameIdToRemove] && this.games[pendingGameIdToRemove].playerCount === 1) {
|
||
console.log(`[GameManager] Игрок ${socketId} (identifier: ${identifier}) отключился, удаляем его ожидающую игру ${pendingGameIdToRemove}`);
|
||
delete this.games[pendingGameIdToRemove];
|
||
const idx = this.pendingPvPGames.indexOf(pendingGameIdToRemove);
|
||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
||
delete this.userToPendingGame[identifier];
|
||
this.broadcastAvailablePvPGames();
|
||
}
|
||
}
|
||
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 firstPlayerUsername = 'Игрок';
|
||
let firstPlayerCharacterName = '';
|
||
|
||
if (game.players && Object.keys(game.players).length > 0) {
|
||
const firstPlayerSocketId = Object.keys(game.players)[0];
|
||
const firstPlayerInfo = game.players[firstPlayerSocketId];
|
||
|
||
if (firstPlayerInfo) {
|
||
if (firstPlayerInfo.socket?.userData?.username) {
|
||
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
|
||
}
|
||
|
||
const charKey = firstPlayerInfo.chosenCharacterKey;
|
||
if (charKey) {
|
||
let charBaseStats;
|
||
if (charKey === 'elena') {
|
||
charBaseStats = gameData.playerBaseStats;
|
||
} else if (charKey === 'almagest') {
|
||
charBaseStats = gameData.almagestBaseStats;
|
||
}
|
||
// Баларда не должно быть в pending PvP как создателя
|
||
|
||
if (charBaseStats && charBaseStats.name) {
|
||
firstPlayerCharacterName = charBaseStats.name;
|
||
} else {
|
||
console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`);
|
||
firstPlayerCharacterName = charKey; // В крайнем случае ключ
|
||
}
|
||
} else {
|
||
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
|
||
if (firstPlayerCharacterName) {
|
||
statusString += ` за ${firstPlayerCharacterName}`;
|
||
}
|
||
statusString += `)`;
|
||
|
||
return {
|
||
id: gameId,
|
||
status: statusString
|
||
};
|
||
}
|
||
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 === 'elena' ? 'playerBaseStats' : (game.playerCharacterKey === 'almagest' ? 'almagestBaseStats' : null)]?.name : 'N/A');
|
||
let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey === 'elena' ? 'playerBaseStats' : (game.opponentCharacterKey === 'almagest' ? 'almagestBaseStats' : (game.opponentCharacterKey === 'balard' ? 'opponentBaseStats' : null))]?.name : 'N/A');
|
||
|
||
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey && game.gameState && !game.gameState.isGameOver) {
|
||
opponentSlotChar = 'Ожидание...';
|
||
}
|
||
|
||
return {
|
||
id: game.id.substring(0,8),
|
||
mode: game.mode,
|
||
playerCount: game.playerCount,
|
||
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A',
|
||
playerSlot: playerSlotChar,
|
||
opponentSlot: opponentSlotChar,
|
||
ownerUserId: game.ownerUserId || 'N/A',
|
||
pending: this.pendingPvPGames.includes(game.id)
|
||
};
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = GameManager; |