bc/server_modules/gameManager.js

311 lines
18 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'); // Нужен для 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;