1116 lines
89 KiB
JavaScript
1116 lines
89 KiB
JavaScript
// /server_modules/gameInstance.js
|
||
const { v4: uuidv4 } = require('uuid'); // Убедитесь, что uuidv4 установлен: npm install uuid
|
||
// Removed the self-import: const GameInstance = require('./gameInstance'); // Не нужно импортировать себя
|
||
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient и данных персонажей
|
||
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
||
const serverGameLogic = require('./gameLogic'); // Подключаем игровую логику
|
||
|
||
class GameInstance {
|
||
// ДОБАВЛЕН АРГУМЕНТ gameManager В КОНСТРУКТОР
|
||
constructor(gameId, io, mode = 'ai', gameManager) {
|
||
this.id = gameId;
|
||
this.io = io; // Ссылка на Socket.IO сервер для широковещательных рассылок
|
||
this.mode = mode; // 'ai' или 'pvp'
|
||
// players теперь { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest', identifier: userId|socketId } }
|
||
this.players = {};
|
||
// playerSockets { 'player': socketObject, 'opponent': socketObject } - для быстрого доступа к сокету по роли
|
||
// Этот маппинг хранит *текущие* сокеты. При переподключении ссылка обновляется в GameManager.
|
||
this.playerSockets = {};
|
||
this.playerCount = 0; // Количество подключенных сокетов (активных игроков) в игре
|
||
|
||
this.gameState = null; // Хранит текущее состояние игры (HP, ресурсы, эффекты, чей ход и т.д.)
|
||
this.aiOpponent = (mode === 'ai'); // Флаг, является ли оппонент AI
|
||
|
||
this.logBuffer = []; // Буфер для сообщений лога боя (ожидающих отправки клиентам)
|
||
|
||
// Ключи персонажей для слотов 'player' и 'opponent' в текущей игре
|
||
// Определяются один раз при инициализации gameState
|
||
this.playerCharacterKey = null; // Ключ персонажа в слоте 'player' (Елена или Альмагест)
|
||
this.opponentCharacterKey = null; // Ключ персонажа в слоте 'opponent' (Балард, Елена или Альмагест)
|
||
|
||
this.ownerIdentifier = null; // Идентификатор создателя игры (userId или socketId)
|
||
|
||
// СОХРАНЯЕМ ССЫЛКУ НА GAMEMANAGER
|
||
this.gameManager = gameManager;
|
||
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: GameInstance created without valid GameManager reference! Cleanup will fail.`);
|
||
// Если GameManager не передан или не имеет метода _cleanupGame, логика очистки не будет работать.
|
||
// В продакшене, возможно, стоит выбрасывать ошибку или завершать процесс.
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Добавляет игрока в игру.
|
||
* Вызывается GameManager при создании новой игры или присоединении второго игрока.
|
||
* @param {object} socket - Объект сокета игрока.
|
||
* @param {string} [chosenCharacterKey='elena'] - Выбранный игроком персонаж (только для первого игрока в PvP).
|
||
* @param {string|number} identifier - Идентификатор пользователя (userId или socketId).
|
||
* @returns {boolean} true, если игрок успешно добавлен, иначе false.
|
||
*/
|
||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) { // <--- ДОБАВЛЕН identifier
|
||
// Проверка, не пытается ли сокет, который уже есть в этой игре, добавиться снова
|
||
if (this.players[socket.id]) {
|
||
console.warn(`[Game ${this.id}] Игрок ${identifier} (сокет ${socket.id}) попытался присоединиться к игре, в которой его сокет уже зарегистрирован.`);
|
||
// Возможно, это быстрый повторный запрос.
|
||
// Отправляем gameError и возвращаем false.
|
||
socket.emit('gameError', { message: 'Ваш сокет уже зарегистрирован в этой игре.' });
|
||
return false;
|
||
}
|
||
// Проверка, не пытается ли пользователь, который уже есть в этой игре (но с другим сокетом), добавиться через addPlayer
|
||
// Этого не должно случаться, если GameManager.handleRequestGameState корректно обновляет сокет для существующего identifier.
|
||
// AddPlayer вызывается только для *нового* игрока в GameInstance.
|
||
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
|
||
if (existingPlayerByIdentifier) {
|
||
console.warn(`[Game ${this.id}] Игрок с идентификатором ${identifier} (${socket.id}) уже находится в игре под сокетом ${existingPlayerByIdentifier.socket.id}. Игнорируем добавление.`);
|
||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре под другим подключением.' });
|
||
return false;
|
||
}
|
||
|
||
|
||
if (this.playerCount >= 2) {
|
||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||
return false;
|
||
}
|
||
|
||
let assignedPlayerId; // 'player' или 'opponent' (технический ID слота в рамках этой игры)
|
||
let actualCharacterKey; // 'elena', 'almagest' (balard только для AI-опонента)
|
||
|
||
if (this.mode === 'ai') {
|
||
if (this.playerCount > 0) { // AI игра только для одного игрока
|
||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||
return false;
|
||
}
|
||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||
actualCharacterKey = 'elena'; // В AI режиме игрок всегда Елена
|
||
this.ownerIdentifier = identifier; // Сохраняем идентификатор создателя AI игры
|
||
|
||
} else { // PvP режим
|
||
if (this.playerCount === 0) { // Первый игрок в PvP
|
||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
|
||
this.ownerIdentifier = identifier; // Сохраняем идентификатор создателя PvP игры
|
||
} else { // Segundo jugador en PvP
|
||
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
||
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); // Находим первого игрока по его роли
|
||
// Segundo jugador recibe automáticamente un personaje "espejo" del primero.
|
||
actualCharacterKey = (firstPlayerInfo?.chosenCharacterKey === 'elena') ? 'almagest' : 'elena';
|
||
// El identificador de ownerIdentifier sigue siendo el del creador de la partida (el primer jugador).
|
||
}
|
||
}
|
||
|
||
// Добавляем информацию об игроке в map players, ключом является socket.id
|
||
this.players[socket.id] = {
|
||
id: assignedPlayerId, // Технический ID слота ('player' или 'opponent')
|
||
socket: socket, // Объект сокета
|
||
chosenCharacterKey: actualCharacterKey, // Выбранный/назначенный персонаж
|
||
identifier: identifier // <--- ДОБАВЛЕНО: Идентификатор пользователя
|
||
};
|
||
this.playerSockets[assignedPlayerId] = socket; // Связываем роль в игре с объектом сокета
|
||
this.playerCount++; // Увеличиваем количество активных сокетов
|
||
|
||
socket.join(this.id); // Присоединяем сокет к комнате игры Socket.IO
|
||
|
||
const characterData = this._getCharacterBaseData(actualCharacterKey);
|
||
console.log(`[Game ${this.id}] Игрок ${identifier} (сокет ${socket.id}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerIdentifier || 'N/A'}`);
|
||
|
||
// Логика старта игры перемещена в GameManager после вызова addPlayer.
|
||
// Логика уведомления "Ожидание оппонента" тоже перемещена в GameManager.
|
||
|
||
return true; // Игрок успешно добавлен
|
||
}
|
||
|
||
/**
|
||
* Удаляет игрока из игры (например, при дисконнекте сокета).
|
||
* Вызывается GameManager при событии 'disconnect'.
|
||
* @param {string} socketId - ID сокета игрока, который отключился.
|
||
*/
|
||
removePlayer(socketId) { // <--- Принимает socketId
|
||
const playerInfo = this.players[socketId]; // Находим информацию об игроке по socketId
|
||
if (playerInfo) {
|
||
const playerRole = playerInfo.id; // 'player' or 'opponent'
|
||
const identifierOfLeavingPlayer = playerInfo.identifier; // Идентификатор уходящего игрока
|
||
const characterKeyOfLeavingPlayer = playerInfo.chosenCharacterKey;
|
||
|
||
const characterData = this._getCharacterBaseData(characterKeyOfLeavingPlayer);
|
||
console.log(`[Game ${this.id}] Игрок ${identifierOfLeavingPlayer} (сокет: ${socketId}, роль: ${playerRole}, персонаж: ${characterKeyOfLeavingPlayer || 'N/A'}) покинул игру.`);
|
||
|
||
// Удаляем сокет из комнаты Socket.IO (если сокет объект еще валиден)
|
||
if (playerInfo.socket) {
|
||
// Проверяем, что это именно тот сокет, который отключился,
|
||
// чтобы не пытаться удалить сокет, если он уже был обновлен на новый при переподключении.
|
||
const actualSocket = this.io.sockets.sockets.get(socketId);
|
||
if (actualSocket && actualSocket.id === socketId) { // Убедимся, что это тот же сокет
|
||
actualSocket.leave(this.id);
|
||
} else if (playerInfo.socket.id === socketId) { // Если сокет объект в playerInfo совпадает с socketId
|
||
// Это может произойти, если Socket.IO уже удалил сокет из io.sockets.sockets,
|
||
// но ссылка в playerInfo еще существует.
|
||
// Попробуем выполнить leave на старом объекте сокета (может не работать, но безопасно)
|
||
try { playerInfo.socket.leave(this.id); } catch(e) { console.warn(`[Game ${this.id}] Error leaving room for old socket ${socketId}: ${e.message}`); }
|
||
}
|
||
}
|
||
|
||
// Удаляем запись об игроке из map players по socketId
|
||
delete this.players[socketId];
|
||
this.playerCount--; // Уменьшаем количество активных сокетов (сокетов, подключенных к игре)
|
||
|
||
// Удаляем ссылку на сокет из playerSockets по роли, ТОЛЬКО если она указывала на отключившийся сокет
|
||
if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) {
|
||
delete this.playerSockets[playerRole];
|
||
}
|
||
|
||
// Логика завершения игры при дисконнекте полностью в GameManager.handleDisconnect.
|
||
// GameManager проверит playerCount после вызова removePlayer и примет решение.
|
||
// GameManager также управляет userIdentifierToGameId и pendingPvPGames.
|
||
|
||
// Если игра была активна и еще не окончена, GameManager вызовет endGameDueToDisconnect - Moved
|
||
} else {
|
||
console.warn(`[Game ${this.id}] removePlayer called for unknown socketId ${socketId}.`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Инициализирует или перезапускает состояние игры.
|
||
* Вызывается GameManager, когда игра полностью готова (PvP 2 игрока, AI 1 игрок) ИЛИ
|
||
* для частичной инициализации PvP игры с 1 игроком.
|
||
* @returns {boolean} true, если оба бойца определены и gameState создан корректно, иначе false.
|
||
*/
|
||
initializeGame() {
|
||
console.log(`[Game ${this.id}] Initializing game state. Mode: ${this.mode}. Current PlayerCount: ${this.playerCount}.`);
|
||
|
||
// Определяем ключи персонажей для слотов 'player' и 'opponent' в этой игре
|
||
// В AI режиме всегда Елена vs Балард
|
||
if (this.mode === 'ai' && this.playerCount === 1) {
|
||
this.playerCharacterKey = 'elena';
|
||
this.opponentCharacterKey = 'balard';
|
||
} else if (this.mode === 'pvp' && this.playerCount === 2) { // PvP режим с 2 игроками
|
||
// Находим информацию об игроках по их ролям
|
||
const player1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
const player2Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||
|
||
// Первый игрок (в слоте PLAYER_ID) сохраняет свой выбранный персонаж
|
||
this.playerCharacterKey = player1Info?.chosenCharacterKey || 'elena'; // Фоллбэк
|
||
|
||
// Второй игрок (в слоте OPPONENT_ID) имеет зеркального персонажа
|
||
const expectedOpponentKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
|
||
// Убеждаемся, что у второго игрока действительно назначен этот персонаж.
|
||
// В addPlayer мы уже стараемся назначить правильно, но тут финальное подтверждение.
|
||
if (player2Info && player2Info.chosenCharacterKey !== expectedOpponentKey) {
|
||
console.warn(`[Game ${this.id}] initializeGame: Expected opponent character ${expectedOpponentKey} but player2Info had ${player2Info.chosenCharacterKey}.`);
|
||
// Не меняем chosenCharacterKey здесь, это данные игрока.
|
||
// Просто используем expectedOpponentKey для определения персонажа в слоте gameState.
|
||
}
|
||
this.opponentCharacterKey = expectedOpponentKey; // Устанавливаем ключ для слота
|
||
|
||
} else if (this.mode === 'pvp' && this.playerCount === 1) {
|
||
// PvP игра ожидает второго игрока. Инициализируем gameState только для Player 1.
|
||
const player1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||
this.playerCharacterKey = player1Info?.chosenCharacterKey || 'elena';
|
||
this.opponentCharacterKey = null; // Оппонент еще не определен
|
||
|
||
} else {
|
||
console.error(`[Game ${this.id}] Unexpected state for initialization! Mode: ${this.mode}, PlayerCount: ${this.playerCount}. Cannot initialize gameState.`);
|
||
this.gameState = null; // Не создаем gameState
|
||
return false; // <-- Возвращаем false при ошибке
|
||
}
|
||
|
||
console.log(`[Game ${this.id}] Finalizing characters for gameState - Player Slot ('${GAME_CONFIG.PLAYER_ID}'): ${this.playerCharacterKey}, Opponent Slot ('${GAME_CONFIG.OPPONENT_ID}'): ${this.opponentCharacterKey || 'N/A (Waiting)'}`);
|
||
|
||
const playerBase = this._getCharacterBaseData(this.playerCharacterKey);
|
||
const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey);
|
||
let opponentBase = null;
|
||
let opponentAbilities = null;
|
||
|
||
// Загружаем данные оппонента, только если он определен (т.е. PvP игра с 2 игроками или AI игра)
|
||
const isOpponentDefined = !!this.opponentCharacterKey;
|
||
if (isOpponentDefined) {
|
||
opponentBase = this._getCharacterBaseData(this.opponentCharacterKey);
|
||
opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey);
|
||
}
|
||
|
||
// Проверяем наличие данных для создания базового gameState
|
||
if (!playerBase || !playerAbilities) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: initializeGame - Failed to load player character data! PlayerKey: ${this.playerCharacterKey}`);
|
||
this.logBuffer = []; // Очищаем лог
|
||
this.addToLog('Критическая ошибка сервера при инициализации персонажа игрока!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
// Уведомляем игроков в комнате об ошибке (если есть)
|
||
Object.values(this.players).forEach(p => p.socket.emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры. Не удалось загрузить данные персонажа игрока.' }));
|
||
this.gameState = null; // Не создаем gameState
|
||
return false; // <-- Возвращаем false при ошибке
|
||
}
|
||
|
||
// Проверяем наличие данных оппонента, если он должен быть определен
|
||
if (isOpponentDefined && (!opponentBase || !opponentAbilities)) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: initializeGame - Failed to load opponent character data! OpponentKey: ${this.opponentCharacterKey}`);
|
||
this.logBuffer = [];
|
||
this.addToLog('Критическая ошибка сервера при инициализации персонажа оппонента!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
Object.values(this.players).forEach(p => p.socket.emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры. Не удалось загрузить данные персонажа оппонента.' }));
|
||
this.gameState = null;
|
||
return false; // <-- Возвращаем false при ошибке
|
||
}
|
||
|
||
// Проверка, если оппонент определен, что у него есть maxHp > 0 (для startGame)
|
||
if (isOpponentDefined && (!opponentBase.maxHp || opponentBase.maxHp <= 0)) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: initializeGame - Opponent has invalid maxHp (${opponentBase.maxHp}) for key ${this.opponentCharacterKey}.`);
|
||
this.logBuffer = [];
|
||
this.addToLog('Критическая ошибка сервера: некорректные данные оппонента!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
Object.values(this.players).forEach(p => p.socket.emit('gameError', { message: 'Критическая ошибка сервера: некорректные данные персонажа оппонента.' }));
|
||
this.gameState = null;
|
||
return false; // <-- Возвращаем false при ошибке
|
||
}
|
||
|
||
|
||
// Создаем начальное gameState
|
||
this.gameState = {
|
||
player: { // Состояние игрока в слоте 'player'
|
||
id: GAME_CONFIG.PLAYER_ID, characterKey: this.playerCharacterKey, name: playerBase.name,
|
||
currentHp: playerBase.maxHp, maxHp: playerBase.maxHp,
|
||
currentResource: playerBase.maxResource, maxResource: playerBase.maxResource,
|
||
resourceName: playerBase.resourceName, attackPower: playerBase.attackPower,
|
||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
|
||
},
|
||
opponent: { // Состояние оппонента в слоте 'opponent'. Если оппонент еще не определен (PvP ожидание), будут плейсхолдеры.
|
||
id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey,
|
||
name: opponentBase?.name || 'Ожидание игрока...',
|
||
currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1, // HP > 0, чтобы не триггерить победу сразу, если оппонент не определен
|
||
currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0,
|
||
resourceName: opponentBase?.resourceName || 'Ресурс', attackPower: opponentBase?.attackPower || 0,
|
||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {},
|
||
// Специальные кулдауны для Баларда (AI) - undefined для других персонажей
|
||
silenceCooldownTurns: (this.opponentCharacterKey === 'balard') ? 0 : undefined,
|
||
manaDrainCooldownTurns: (this.opponentCharacterKey === 'balard') ? 0 : undefined,
|
||
},
|
||
// Случайный первый ход только если оба игрока (бойца) определены (т.е. игра готова к старту)
|
||
isPlayerTurn: isOpponentDefined ? Math.random() < 0.5 : true, // Если ожидаем, всегда ход первого игрока (кто создал)
|
||
isGameOver: false,
|
||
turnNumber: 1,
|
||
gameMode: this.mode
|
||
};
|
||
|
||
// Инициализация кулдаунов способностей для обоих бойцов (если они определены)
|
||
// Для игрока (player slot)
|
||
if (playerAbilities) {
|
||
playerAbilities.forEach(ability => {
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||
this.gameState.player.abilityCooldowns[ability.id] = 0;
|
||
}
|
||
});
|
||
} else {
|
||
console.error(`[Game ${this.id}] Cannot initialize player abilities cooldowns: playerAbilities is null.`);
|
||
}
|
||
|
||
// Для оппонента (opponent slot), только если он определен
|
||
if (isOpponentDefined && opponentAbilities) {
|
||
opponentAbilities.forEach(ability => {
|
||
let cd = 0;
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||
cd = ability.cooldown;
|
||
}
|
||
if (this.opponentCharacterKey === 'balard') { // Специальные КД для Баларда
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && typeof GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN === 'number') {
|
||
cd = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;
|
||
} else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') {
|
||
cd = ability.internalCooldownValue;
|
||
}
|
||
}
|
||
if (cd > 0) {
|
||
this.gameState.opponent.abilityCooldowns[ability.id] = 0;
|
||
}
|
||
});
|
||
} else if (isOpponentDefined) {
|
||
console.warn(`[Game ${this.id}] Cannot initialize opponent abilities cooldowns: opponentAbilities is null for key ${this.opponentCharacterKey}.`);
|
||
}
|
||
|
||
|
||
// Добавляем начальное сообщение в лог и стартовую насмешку, только если игра полностью готова к старту (оба бойца определены)
|
||
if (isOpponentDefined) { // Проверяем, определен ли оппонент (значит, игра полностью готова)
|
||
const isRestart = this.logBuffer.length > 0; // Сейчас этот буфер всегда пустой при инициализации новой игры
|
||
this.logBuffer = []; // Очищаем лог перед новой игрой
|
||
|
||
this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
|
||
// Добавляем стартовую насмешку игрока (в слоте 'player'), если она есть
|
||
const playerCharKey = this.gameState.player.characterKey;
|
||
if (playerCharKey === 'elena' || playerCharKey === 'almagest') {
|
||
// getRandomTaunt теперь принимает gameDataForLogic и gameState
|
||
const startTaunt = serverGameLogic.getRandomTaunt(playerCharKey, 'battleStart', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (startTaunt !== "(Молчание)") this.addToLog(`${this.gameState.player.name}: "${startTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
// Если оппонент не определен (PvP ожидание),gameState инициализируется частично, но не считается готовым к старту.
|
||
// LogBuffer не очищается, waitingForOpponent отправлено клиенту.
|
||
console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState?.isGameOver}. First turn (if ready): ${this.gameState?.isPlayerTurn ? this.gameState?.player?.name : (this.gameState?.opponent?.name || 'Оппонент')}. Opponent Defined (Ready for Start): ${isOpponentDefined}`);
|
||
|
||
// Возвращаем флаг, готово ли gameState к началу игры (т.е. определены ли оба бойца)
|
||
return isOpponentDefined; // <-- Важно: возвращаем, готова ли игра к старту
|
||
}
|
||
|
||
/**
|
||
* Запускает игру (отправляет начальное состояние клиентам и начинает первый ход).
|
||
* Вызывается GameManager после успешной инициализации gameState и наличия 2 игроков (PvP) или 1 (AI),
|
||
* и если initializeGame вернул true.
|
||
*/
|
||
startGame() {
|
||
// Проверяем, что игра полностью готова к запуску (gameState инициализирован с обоими персонажами)
|
||
if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание игрока...' || !this.gameState.opponent.maxHp || this.gameState.opponent.maxHp <= 0) {
|
||
console.error(`[Game ${this.id}] startGame: Game state is not fully ready for start. Aborting.`);
|
||
// initializeGame должен был вернуть false и добавить ошибку в лог.
|
||
// GameManager должен был вызвать cleanupGame при ошибке initializeGame.
|
||
return;
|
||
}
|
||
// Убеждаемся, что у нас есть 1 или 2 игрока (сокета) - GameManager должен гарантировать это перед вызовом startGame
|
||
if (this.playerCount === 0 || (this.mode === 'pvp' && this.playerCount === 1)) {
|
||
console.warn(`[Game ${this.id}] startGame called with insufficient players (${this.playerCount}). Mode: ${this.mode}. Aborting start.`);
|
||
return;
|
||
}
|
||
|
||
|
||
console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver: ${this.gameState.isGameOver}`);
|
||
|
||
// Получаем данные персонажей для отправки клиентам
|
||
const playerCharDataForSlotPlayer = this._getCharacterData(this.playerCharacterKey);
|
||
const opponentCharDataForSlotOpponent = this._getCharacterData(this.opponentCharacterKey);
|
||
|
||
if (!playerCharDataForSlotPlayer || !opponentCharDataForSlotOpponent) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: startGame - Failed to load character data! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`);
|
||
this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при старте игры (не удалось загрузить данные персонажей).' });
|
||
// Критическая ошибка, игра должна быть очищена
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'start_data_load_failed');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Отправляем каждому игроку его персональные данные для игры и начальное состояние
|
||
Object.values(this.players).forEach(playerInfo => {
|
||
// Проверяем, что сокет игрока еще подключен
|
||
if (playerInfo.socket && playerInfo.socket.connected) {
|
||
let dataForThisClient;
|
||
// playerInfo.id - это технический ID слота этого конкретного клиента в рамках игры ('player' или 'opponent')
|
||
if (playerInfo.id === GAME_CONFIG.PLAYER_ID) { // Этот клиент играет за слот 'player'
|
||
dataForThisClient = {
|
||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||
playerBaseStats: playerCharDataForSlotPlayer.baseStats, opponentBaseStats: opponentCharDataForSlotOpponent.baseStats,
|
||
playerAbilities: playerCharDataForSlotPlayer.abilities, opponentAbilities: opponentCharDataForSlotOpponent.abilities,
|
||
log: this.consumeLogBuffer(), // Игрок в слоте player получает весь накопленный лог
|
||
clientConfig: { ...GAME_CONFIG } // Копия конфига для клиента
|
||
};
|
||
} else { // Этот клиент играет за слот 'opponent' (только в PvP)
|
||
dataForThisClient = {
|
||
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
|
||
// Меняем местами статы и абилки, чтобы клиент видел себя как 'player', а противника как 'opponent'
|
||
playerBaseStats: opponentCharDataForSlotOpponent.baseStats, opponentBaseStats: playerCharDataForSlotPlayer.baseStats,
|
||
playerAbilities: opponentCharDataForSlotOpponent.abilities, opponentAbilities: playerCharDataForSlotPlayer.abilities,
|
||
log: this.consumeLogBuffer(), // Второй игрок тоже получает весь накопленный лог
|
||
clientConfig: { ...GAME_CONFIG }
|
||
};
|
||
}
|
||
playerInfo.socket.emit('gameStarted', dataForThisClient);
|
||
console.log(`[Game ${this.id}] Sent gameStarted to ${playerInfo.identifier} (socket ${playerInfo.socket.id}).`);
|
||
} else {
|
||
console.warn(`[Game ${this.id}] Player ${playerInfo.identifier} (socket ${playerInfo.socket?.id}) is disconnected. Cannot send gameStarted.`);
|
||
// Если один из игроков отключен в момент старта PvP игры, она должна быть завершена.
|
||
// GameManager.handleDisconnect уже должен был вызвать endGameDueToDisconnect.
|
||
}
|
||
});
|
||
|
||
// Добавляем лог о начале хода после отправки gameStarted, чтобы лог был актуальным
|
||
const firstTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${firstTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||
// Отправляем этот лог всем клиентам
|
||
this.broadcastLogUpdate();
|
||
|
||
|
||
// Если ход AI, запускаем его логику с небольшой задержкой
|
||
// AI Балард всегда в слоте 'opponent' в AI режиме, и его characterKey === 'balard'
|
||
if (!this.gameState.isPlayerTurn && this.aiOpponent && this.opponentCharacterKey === 'balard') {
|
||
console.log(`[Game ${this.id}] AI (Балард) ходит первым. Запускаем AI turn.`);
|
||
// Небольшая задержка для старта AI хода, чтобы клиент успел отрисовать
|
||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN || 1200);
|
||
} else { // Ход реального игрока (первого или второго в PvP)
|
||
// Игрок, чей ход, получит индикатор хода и активные кнопки через gameStateUpdate в клиентском ui.js
|
||
console.log(`[Game ${this.id}] Ход реального игрока ${firstTurnActorState.name} (роль: ${firstTurnActorState.id}).`);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Обрабатывает действие игрока, полученное от клиента.
|
||
* Вызывается GameManager при событии 'playerAction'.
|
||
* @param {string} requestingSocketId - ID сокета игрока, запросившего действие.
|
||
* @param {object} actionData - Данные о действии ({ actionType: 'attack' | 'ability', abilityId?: string }).
|
||
*/
|
||
processPlayerAction(requestingSocketId, actionData) {
|
||
// Проверяем, что игра активна и не окончена
|
||
if (!this.gameState || this.gameState.isGameOver) {
|
||
const playerSocket = this.io.sockets.sockets.get(requestingSocketId);
|
||
if (playerSocket) playerSocket.emit('gameError', { message: 'Игра уже завершена или неактивна.' });
|
||
return;
|
||
}
|
||
|
||
// Находим информацию об игроке по socketId (используем текущий socketId, т.к. он пришел в запросе)
|
||
const actingPlayerInfo = this.players[requestingSocketId];
|
||
if (!actingPlayerInfo) {
|
||
// Этого не должно происходить, если GameManager корректно ищет игру по идентификатору и передает
|
||
// текущий socketId. Но если произошло, возможно, сокет отправил действие после удаления из players.
|
||
console.error(`[Game ${this.id}] Action from socket ${requestingSocketId} not found in players map.`);
|
||
const playerSocket = this.io.sockets.sockets.get(requestingSocketId);
|
||
if (playerSocket && playerSocket.connected) playerSocket.disconnect(true); // Отключаем подозрительный сокет
|
||
return;
|
||
}
|
||
|
||
const actingPlayerRole = actingPlayerInfo.id; // 'player' или 'opponent' (технический ID слота)
|
||
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
|
||
(!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID);
|
||
|
||
if (!isCorrectTurn) {
|
||
// Неправильный ход - просто игнорируем действие. Клиентский UI должен предотвращать такое.
|
||
// actingPlayerInfo.socket.emit('gameError', { message: "Сейчас не ваш ход!" }); // Можно отправлять ошибку, но это спамит лог
|
||
console.warn(`[Game ${this.id}] Action from ${actingPlayerInfo.identifier} (socket ${requestingSocketId}): Not their turn.`);
|
||
return; // Игнорируем действие
|
||
}
|
||
|
||
const attackerState = this.gameState[actingPlayerRole]; // Состояние того, кто ходит (в gameState по роли)
|
||
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const defenderState = this.gameState[defenderRole]; // Состояние противника (в gameState по роли)
|
||
|
||
// Получаем базовые данные персонажей для логики
|
||
const attackerData = this._getCharacterData(attackerState.characterKey);
|
||
const defenderData = this._getCharacterData(defenderState.characterKey);
|
||
|
||
if (!attackerData || !defenderData) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: processPlayerAction - Failed to load character data! AttackerKey: ${attackerState.characterKey}, DefenderKey: ${defenderState.characterKey}`);
|
||
this.addToLog('Критическая ошибка сервера при обработке действия (не найдены данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
this.broadcastLogUpdate(); // Уведомляем клиентов об ошибке через лог
|
||
// Критическая ошибка, игра должна быть очищена
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'action_data_load_failed');
|
||
}
|
||
return;
|
||
}
|
||
let actionValid = true; // Флаг валидности действия (логической, не очередности хода)
|
||
|
||
// --- Обработка базовой атаки ---
|
||
if (actionData.actionType === 'attack') {
|
||
// Добавляем насмешку при базовой атаке (если есть такие для говорящего)
|
||
// Насмешки при атаке могут быть специфичны для говорящего и противника
|
||
// getRandomTaunt теперь принимает gameDataForLogic и gameState
|
||
const taunt = serverGameLogic.getRandomTaunt(attackerState.characterKey, 'basicAttack', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
|
||
// Выполняем логику атаки через gameLogic
|
||
// performAttack теперь принимает gameDataForLogic и gameState
|
||
serverGameLogic.performAttack(
|
||
attackerState, defenderState, attackerData.baseStats, defenderData.baseStats,
|
||
this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData
|
||
);
|
||
|
||
// Логика для "Силы Природы" и аналогов - бафф применяется после атаки
|
||
// Проверяем, есть ли активный "отложенный" бафф (isDelayed) на атакующем
|
||
const delayedAttackBuffEffect = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK));
|
||
|
||
if (delayedAttackBuffEffect && !delayedAttackBuffEffect.justCast) { // Если эффект активен и не только что наложен в этом ходу
|
||
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerData.baseStats.maxResource - attackerState.currentResource);
|
||
if (actualRegen > 0) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
|
||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${delayedAttackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
|
||
}
|
||
}
|
||
|
||
|
||
// --- Обработка использования способности ---
|
||
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
|
||
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
|
||
if (!ability) {
|
||
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
|
||
console.warn(`[Game ${this.id}] Игрок ${actingPlayerInfo.identifier} (сокет ${requestingSocketId}) попытался использовать неизвестную способность ID: ${actionData.abilityId}.`);
|
||
return; // Неизвестная способность
|
||
}
|
||
|
||
// Проверки валидности использования способности (ресурс, КД, безмолвие и т.д.)
|
||
// Эти проверки дублируют клиентские, но нужны на сервере для безопасности
|
||
const hasEnoughResource = attackerState.currentResource >= ability.cost;
|
||
const isOnCooldown = (attackerState.abilityCooldowns?.[ability.id] || 0) > 0;
|
||
const isCasterFullySilenced = attackerState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||
const isAbilitySpecificallySilenced = attackerState.disabledAbilities?.some(dis => dis.abilityId === ability.id && dis.turnsLeft > 0);
|
||
const isSilenced = isCasterFullySilenced || isAbilitySpecificallySilenced;
|
||
|
||
// Специальные КД для Баларда (AI) - эти поля undefined для других персонажей
|
||
let isOnSpecialCooldown = false;
|
||
if (attackerState.characterKey === 'balard') {
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns !== undefined && attackerState.silenceCooldownTurns > 0) isOnSpecialCooldown = true;
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns !== undefined && attackerState.manaDrainCooldownTurns > 0) isOnSpecialCooldown = true;
|
||
}
|
||
|
||
// Нельзя кастовать бафф, если он уже активен (для баффов, которые не должны стакаться)
|
||
const isBuffAlreadyActive = ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id);
|
||
|
||
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
|
||
const isTargetedDebuff = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF;
|
||
const effectIdForDebuff = 'effect_' + ability.id;
|
||
const isDebuffAlreadyOnTarget = isTargetedDebuff && defenderState.activeEffects.some(e => e.id === effectIdForDebuff);
|
||
|
||
|
||
// Проверка всех условий
|
||
if (!hasEnoughResource) { this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && (isOnCooldown || isOnSpecialCooldown)) { this.addToLog(`"${ability.name}" еще на перезарядке.`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && isSilenced) { this.addToLog(`${attackerState.name} не может использовать способности из-за безмолвия!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && isBuffAlreadyActive) { this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
if (actionValid && isDebuffAlreadyOnTarget) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||
|
||
|
||
// Если все проверки пройдены, действие считается логически валидным
|
||
if (actionValid) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost); // Расходуем ресурс
|
||
|
||
// Добавляем насмешку ПРИ КАСТЕ СПОСОБНОСТИ (если есть такие для говорящего)
|
||
// getRandomTaunt теперь принимает gameDataForLogic и gameState
|
||
const taunt = serverGameLogic.getRandomTaunt(attackerState.characterKey, 'selfCastAbility', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
|
||
|
||
// Применяем эффекты способности через gameLogic
|
||
// applyAbilityEffect теперь принимает gameDataForLogic и gameState
|
||
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
|
||
// Установка кулдауна способности (после успешного каста)
|
||
let baseCooldown = 0;
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||
baseCooldown = ability.cooldown;
|
||
}
|
||
// Специальные внутренние КД для Баларда - перебивают общий КД, если заданы
|
||
if (attackerState.characterKey === 'balard') {
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && typeof GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN === 'number') {
|
||
attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;
|
||
baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Специальный КД становится актуальным кулдауном
|
||
} else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') {
|
||
attackerState.manaDrainCooldownTurns = ability.internalCooldownValue;
|
||
baseCooldown = ability.internalCooldownValue; // Специальный КД становится актуальным кулдауном
|
||
}
|
||
}
|
||
|
||
if (baseCooldown > 0 && attackerState.abilityCooldowns) {
|
||
// Устанавливаем кулдаун. Добавляем +1, т.к. кулдаун уменьшится в конце этого хода.
|
||
attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
|
||
}
|
||
}
|
||
} else {
|
||
// Неизвестный тип действия
|
||
actingPlayerInfo.socket.emit('gameError', { message: `Неизвестный тип действия: ${actionData?.actionType}` });
|
||
console.warn(`[Game ${this.id}] Получен неизвестный тип действия от ${actingPlayerInfo.identifier} (сокет ${requestingSocketId}): ${actionData?.actionType}`);
|
||
actionValid = false; // Помечаем как невалидное
|
||
}
|
||
|
||
// --- Завершение хода ---
|
||
// Проверяем конец игры после выполнения действия
|
||
if (this.checkGameOver()) {
|
||
this.broadcastGameStateUpdate(); // Отправляем финальное состояние и лог всем
|
||
// Очистка игры теперь происходит ВНУТРИ checkGameOver через вызов _notifyGameEnded
|
||
return; // Если игра окончена, не переключаем ход
|
||
}
|
||
|
||
// Если действие было логически валидным и игра не окончена, переключаем ход с задержкой
|
||
if (actionValid) {
|
||
console.log(`[Game ${this.id}] Player action valid. Switching turn in ${GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500}ms.`);
|
||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500);
|
||
} else {
|
||
// Если действие невалидно, просто обновляем лог (если что-то было добавлено), ход не переключается
|
||
console.log(`[Game ${this.id}] Player action invalid. Broadcasting log update.`);
|
||
this.broadcastLogUpdate();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Переключает ход на следующего бойца, обрабатывает эффекты конца хода и кулдауны.
|
||
*/
|
||
switchTurn() {
|
||
// Проверяем, что игра активна и не окончена
|
||
if (!this.gameState || this.gameState.isGameOver) return; // Не переключаем ход, если игра окончена или состояние некорректно
|
||
|
||
// Определяем бойца, чей ход только что закончился
|
||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
||
const endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey); // Получаем данные персонажа
|
||
|
||
// Проверяем наличие данных для обработки эффектов и КД
|
||
if (!endingTurnCharacterData) {
|
||
console.error(`[Game ${this.id}] SwitchTurn Error: No character data found for ending turn actor role ${endingTurnActorRole} with key ${endingTurnActorState.characterKey}. Cannot process end-of-turn effects.`);
|
||
// Критическая ошибка, игра должна быть очищена
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'switch_turn_data_error');
|
||
}
|
||
return;
|
||
} else {
|
||
// Обработка активных эффектов в конце хода (DoT, HoT, истечение баффов/debuffs) для бойца, чей ход закончился
|
||
// processEffects теперь принимает gameDataForLogic и gameState
|
||
serverGameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnCharacterData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
|
||
// Обновляем статус блока для обоих бойцов после обработки эффектов (т.к. эффекты блока могли закончиться)
|
||
// Важно обновить статус для ОБОИХ, т.к. эффекты на одном могут зависеть от статуса блока другого.
|
||
serverGameLogic.updateBlockingStatus(this.gameState.player);
|
||
serverGameLogic.updateBlockingStatus(this.gameState.opponent);
|
||
|
||
// Уменьшение общих кулдаунов способностей для бойца, чей ход закончился
|
||
if (endingTurnActorState.abilityCooldowns) {
|
||
// processPlayerAbilityCooldowns теперь принимает gameDataForLogic (хотя он там не используется напрямую)
|
||
serverGameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnCharacterData.abilities, endingTurnActorState.name, this.addToLog.bind(this));
|
||
}
|
||
// Специальные КД для Баларда (если он в слоте, который сейчас заканчивает ход)
|
||
if (endingTurnActorState.characterKey === 'balard') {
|
||
if (endingTurnActorState.silenceCooldownTurns !== undefined && endingTurnActorState.silenceCooldownTurns > 0) endingTurnActorState.silenceCooldownTurns--;
|
||
if (endingTurnActorState.manaDrainCooldownTurns !== undefined && endingTurnActorState.manaDrainCooldownTurns > 0) endingTurnActorState.manaDrainCooldownTurns--;
|
||
}
|
||
|
||
// Уменьшение длительности заглушения конкретных способностей (если это ход того, кто был заглушен)
|
||
if (endingTurnActorState.disabledAbilities?.length > 0) {
|
||
const charAbilitiesForDisabledCheck = this._getCharacterAbilities(endingTurnActorState.characterKey); // Список абилок для поиска по ID
|
||
if (charAbilitiesForDisabledCheck) {
|
||
// processDisabledAbilities теперь принимает gameDataForLogic (хотя он там не используется напрямую)
|
||
serverGameLogic.processDisabledAbilities(endingTurnActorState.disabledAbilities, charAbilitiesForDisabledCheck, endingTurnActorState.name, this.addToLog.bind(this));
|
||
} else {
|
||
console.warn(`[Game ${this.id}] SwitchTurn: Cannot process disabledAbilities for ${endingTurnActorState.name}: character abilities data not found.`);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Проверяем конец игры после обработки эффектов конца хода
|
||
if (this.checkGameOver()) {
|
||
this.broadcastGameStateUpdate(); // Отправляем финальное состояние и лог всем
|
||
// Очистка игры теперь происходит ВНУТРИ checkGameOver через вызов _notifyGameEnded
|
||
return; // Если игра окончена, останавливаемся
|
||
}
|
||
|
||
// Если игра не окончена, переключаем ход на следующего бойца с задержкой
|
||
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
|
||
// Если ход переключился на первого игрока (player), увеличиваем номер хода
|
||
if (this.gameState.isPlayerTurn) {
|
||
this.gameState.turnNumber++;
|
||
}
|
||
|
||
// Определяем бойца, чей ход начинается
|
||
const currentTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||
|
||
// Отправляем обновленное состояние игры и лог всем клиентам в комнате
|
||
this.broadcastGameStateUpdate();
|
||
|
||
// Если ход AI, запускаем его логику с задержкой
|
||
// AI Балард всегда в слоте 'opponent' в AI режиме, и его characterKey === 'balard'
|
||
if (!this.gameState.isPlayerTurn && this.aiOpponent && this.opponentCharacterKey === 'balard') {
|
||
console.log(`[Game ${this.id}] Ход AI (Балард). Запускаем AI turn.`);
|
||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN || 1200);
|
||
} else { // Ход реального игрока (первого или второго в PvP)
|
||
// Игрок, чей ход, получит индикатор хода и активные кнопки через gameStateUpdate в клиентском ui.js
|
||
console.log(`[Game ${this.id}] Ход реального игрока ${currentTurnActorState.name} (роль: ${currentTurnActorState.id}).`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает ход AI (Балард).
|
||
* Вызывается из switchTurn, если следующий ход принадлежит AI.
|
||
*/
|
||
processAiTurn() {
|
||
// Проверка: это точно ход AI Баларда и игра активна?
|
||
// AI Балард только в режиме 'ai', и его characterKey в gameState должен быть 'balard'.
|
||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') {
|
||
if(!this.gameState || this.gameState.isGameOver) return; // Если игра закончена, ничего не делаем
|
||
// Если не ход AI или это не AI Балард, выходим (на всякий случай)
|
||
// console.warn(`[Game ${this.id}] processAiTurn called when it's not AI Balard's turn or not AI mode.`);
|
||
// Если AI ход по какой-то причине пропущен, переключаем ход на игрока
|
||
console.warn(`[Game ${this.id}] Skipping AI turn logic (not AI Balard's turn or game not ready). Switching turn.`);
|
||
this.switchTurn(); // Пропускаем AI ход и переключаем обратно
|
||
return;
|
||
}
|
||
|
||
const attackerState = this.gameState.opponent; // AI Балард всегда в слоте 'opponent' в AI режиме
|
||
const defenderState = this.gameState.player; // Игрок всегда в слоте 'player' в AI режиме
|
||
// Получаем базовые данные персонажей для логики
|
||
const attackerData = this._getCharacterData('balard'); // Базовые данные Баларда
|
||
const defenderData = this._getCharacterData('elena'); // Базовые данные Елены (противник AI)
|
||
|
||
if (!attackerData || !defenderData) {
|
||
console.error(`[Game ${this.id}] CRITICAL ERROR: processAiTurn - Failed to load character data!`);
|
||
this.addToLog("AI не может действовать: ошибка данных персонажа.", GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
this.broadcastLogUpdate(); // Отправляем ошибку в лог
|
||
// Критическая ошибка, игра должна быть очищена
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'ai_data_load_failed');
|
||
}
|
||
this.switchTurn(); // Пропускаем ход AI и переключаем обратно на игрока
|
||
return;
|
||
}
|
||
|
||
// Проверка полного безмолвия Баларда (от Гипнотического Взгляда Елены или Раскола Разума Альмагест) - повторная проверка из decideAiAction, на всякий случай
|
||
const isBalardFullySilenced = attackerState.activeEffects.some(
|
||
eff => eff.isFullSilence && eff.turnsLeft > 0
|
||
);
|
||
|
||
if (isBalardFullySilenced) {
|
||
// AI под полным безмолвием просто атакует
|
||
// Лог о безмолвии и атаке в смятении добавляется в processAiTurn перед вызовом performAttack.
|
||
// decideAiAction просто возвращает действие.
|
||
// if (this.logBuffer.filter(log => log.message.includes('под действием Безмолвия')).length === 0) { // Логируем только если еще не логировали в decideAiAction
|
||
// this.addToLog(`😵 ${attackerState.name} под действием Безмолвия! Атакует в смятении.`, GAME_CONFIG.LOG_TYPE_EFFECT);
|
||
// }
|
||
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
|
||
// Логика для "Силы Природы" и аналогов (неактуально для Баларда)
|
||
const delayedAttackBuffEffect = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK));
|
||
if (delayedAttackBuffEffect && !delayedAttackBuffEffect.justCast) {
|
||
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerState.maxResource - attackerState.currentResource);
|
||
if (actualRegen > 0) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
|
||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${delayedAttackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
|
||
}
|
||
}
|
||
|
||
|
||
// После атаки под безмолвием, переключаем ход
|
||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем Game Over
|
||
console.log(`[Game ${this.id}] AI (Балард) attacked while silenced. Switching turn in ${GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500}ms.`);
|
||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500);
|
||
return; // Завершаем обработку AI хода
|
||
}
|
||
|
||
|
||
// AI принимает решение о действии, используя логику из gameLogic
|
||
// decideAiAction теперь принимает gameDataForLogic и gameState
|
||
const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this));
|
||
|
||
// --- Выполнение выбранного AI действия ---
|
||
if (aiDecision.actionType === 'attack') {
|
||
// AI Балард пока не имеет специфических насмешек при базовой атаке
|
||
|
||
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
|
||
// Логика для "Силы Природы" и аналогов (неактуально для Баларда)
|
||
const delayedAttackBuffEffect = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK));
|
||
if (delayedAttackBuffEffect && !delayedAttackBuffEffect.justCast) {
|
||
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerState.maxResource - attackerState.currentResource);
|
||
if (actualRegen > 0) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
|
||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${delayedAttackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
|
||
}
|
||
}
|
||
|
||
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
|
||
const ability = aiDecision.ability;
|
||
// decideAiAction уже проверил ресурс, КД и специфические условия перед тем, как выбрать эту способность.
|
||
// Теперь просто выполняем ее.
|
||
|
||
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost); // Расходуем ресурс
|
||
|
||
// Добавляем насмешку ПРИ КАСТЕ СПОСОБНОСТИ Балардом (если есть такие для говорящего)
|
||
// В текущей реализации Балард не имеет секции selfCastAbility
|
||
// const taunt = serverGameLogic.getRandomTaunt(attackerState.characterKey, 'selfCastAbility', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState);
|
||
// if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
|
||
// applyAbilityEffect теперь принимает gameDataForLogic и gameState
|
||
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||
|
||
// Установка кулдауна способности Баларда (после успешного каста)
|
||
let baseCooldown = 0;
|
||
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
|
||
baseCooldown = ability.cooldown;
|
||
}
|
||
// Специальные внутренние КД для Баларда - перебивают общий КД, если заданы
|
||
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && typeof GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN === 'number') {
|
||
attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;
|
||
baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Специальный КД становится актуальным кулдауном
|
||
} else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') {
|
||
attackerState.manaDrainCooldownTurns = ability.internalCooldownValue;
|
||
baseCooldown = ability.internalCooldownValue; // Специальный КД становится актуальным кулдауном
|
||
}
|
||
|
||
if (baseCooldown > 0 && attackerState.abilityCooldowns) {
|
||
// Устанавливаем кулдаун. Добавляем +1, т.к. кулдаун уменьшится в конце этого хода.
|
||
attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
|
||
}
|
||
|
||
} else if (aiDecision.actionType === 'pass') { // Если AI решил пропустить ход
|
||
if (aiDecision.logMessage) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
|
||
else this.addToLog(`${attackerState.name} обдумывает свой следующий ход...`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
} else { // Неизвестное решение AI или ошибка в логике decideAiAction
|
||
console.error(`[Game ${this.id}] AI (Балард) chose an invalid action type: ${aiDecision.actionType}. Defaulting to pass and logging error.`);
|
||
this.addToLog(`AI ${attackerState.name} не смог выбрать действие из-за ошибки. Пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
// Критическая ошибка в логике AI, возможно, стоит завершить игру?
|
||
// Но пропуск хода - это более мягкое решение.
|
||
}
|
||
|
||
// Проверяем конец игры после выполнения действия AI
|
||
if (this.checkGameOver()) {
|
||
this.broadcastGameStateUpdate(); // Отправляем финальное состояние и лог всем
|
||
// Очистка игры теперь происходит ВНУТРИ checkGameOver через вызов _notifyGameEnded
|
||
return; // Если игра окончена, останавливаемся
|
||
}
|
||
|
||
// Если игра не окончена, переключаем ход на игрока с задержкой
|
||
console.log(`[Game ${this.id}] AI action complete. Switching turn in ${GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500}ms.`);
|
||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION || 500);
|
||
}
|
||
|
||
/**
|
||
* Проверяет, окончена ли игра (по HP бойцов).
|
||
* Если да, обновляет gameState, отправляет событие gameOver и уведомляет GameManager.
|
||
* Вызывается после каждого действия (игрока или AI) и после обработки эффектов конца хода.
|
||
* @returns {boolean} true, если игра окончена, иначе false.
|
||
*/
|
||
checkGameOver() {
|
||
// Проверка на конец игры происходит только если gameState существует и игра еще не помечена как оконченная
|
||
if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true;
|
||
|
||
// Убеждаемся, что оба бойца определены в gameState и не являются плейсхолдерами
|
||
// Проверка maxHp > 0 в gameState.opponent гарантирует, что оппонент не плейсхолдер
|
||
if (!this.gameState.player || !this.gameState.opponent || this.gameState.opponent.maxHp <= 0) {
|
||
// Если один из бойцов не готов (например, PvP игра ожидает второго игрока), игра не может закончиться по HP
|
||
return false;
|
||
}
|
||
|
||
|
||
// Используем внутреннюю функцию gameLogic для проверки условий победы/поражения по HP
|
||
// checkGameOverInternal теперь принимает gameDataForLogic и gameState
|
||
const isOver = serverGameLogic.checkGameOverInternal(this.gameState, GAME_CONFIG, gameData);
|
||
|
||
// Если игра только что стала оконченной (был false, стал true)
|
||
if (isOver && !this.gameState.isGameOver) {
|
||
this.gameState.isGameOver = true; // Устанавливаем флаг
|
||
|
||
const playerDead = this.gameState.player?.currentHp <= 0;
|
||
const opponentDead = this.gameState.opponent?.currentHp <= 0;
|
||
|
||
// Определяем победителя и проигравшего по ролям
|
||
// Если оба мертвы одновременно, побеждает тот, кто не был убит последним действием,
|
||
// или определяется по правилам (здесь: player побеждает при одновременной смерти)
|
||
let winnerRole = null;
|
||
let loserRole = null;
|
||
// В AI режиме победителем всегда считается Player (если выжил), даже если AI тоже погиб.
|
||
// При дисконнекте в AI режиме победителя нет.
|
||
if (this.mode === 'ai') {
|
||
winnerRole = playerDead ? null : GAME_CONFIG.PLAYER_ID; // Player победил, если не умер. AI не "побеждает".
|
||
loserRole = playerDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||
} else { // PvP режим
|
||
if (playerDead && opponentDead) {
|
||
// Оба мертвы, побеждает игрок (по правилам игры)
|
||
winnerRole = GAME_CONFIG.PLAYER_ID;
|
||
loserRole = GAME_CONFIG.OPPONENT_ID;
|
||
} else if (playerDead) {
|
||
// Игрок мертв, побеждает оппонент
|
||
winnerRole = GAME_CONFIG.OPPONENT_ID;
|
||
loserRole = GAME_CONFIG.PLAYER_ID;
|
||
} else if (opponentDead) {
|
||
// Оппонент мертв, побеждает игрок
|
||
winnerRole = GAME_CONFIG.PLAYER_ID;
|
||
loserRole = GAME_CONFIG.OPPONENT_ID;
|
||
} else {
|
||
// Этого не должно произойти, если isOver = true, но никто не мертв
|
||
console.error(`[Game ${this.id}] checkGameOverInternal returned true, but neither fighter is dead. GameState:`, this.gameState);
|
||
this.gameState.isGameOver = false; // Сбрасываем флаг, игра не окончена
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
const winnerState = this.gameState[winnerRole];
|
||
const loserState = this.gameState[loserRole];
|
||
|
||
const winnerName = winnerState?.name || (winnerRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник");
|
||
const loserName = loserState?.name || (loserRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник");
|
||
const loserCharacterKey = loserState?.characterKey || 'unknown'; // Ключ персонажа проигравшего
|
||
|
||
// Добавляем сообщение о победе в лог
|
||
// В AI режиме при победе игрока лог специфичный. Если AI "победил" (игрок умер), лог тоже специфичный.
|
||
if (this.mode === 'ai') {
|
||
if (winnerRole === GAME_CONFIG.PLAYER_ID) { // Игрок победил
|
||
this.addToLog(`🏁 ПОБЕДА! Вы одолели ${loserName}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
} else { // Игрок проиграл AI
|
||
this.addToLog(`😭 ПОРАЖЕНИЕ! ${winnerName} оказался(лась) сильнее! 😭`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
}
|
||
} else { // PvP режим
|
||
this.addToLog(`🏁 ПОБЕДА! ${winnerName} одолел(а) ${loserName}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
}
|
||
|
||
|
||
// Добавляем дополнительные сообщения о конце игры (насмешка победителя)
|
||
// В AI режиме AI Балард не "говорит" в конце. Говорит только игрок, если победил.
|
||
const winningCharacterKey = winnerState?.characterKey;
|
||
if (this.mode === 'ai' && winningCharacterKey === 'elena') {
|
||
const taunt = serverGameLogic.getRandomTaunt(winningCharacterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerName}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
} else if (this.mode === 'pvp' && (winningCharacterKey === 'elena' || winningCharacterKey === 'almagest')) {
|
||
const taunt = serverGameLogic.getRandomTaunt(winningCharacterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState);
|
||
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerName}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
|
||
// Специальные системные логи в конце игры, зависящие от проигравшего
|
||
if (loserCharacterKey === 'balard') {
|
||
this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
} else if (loserCharacterKey === 'almagest') {
|
||
this.addToLog(`Над полем битвы воцаряется тишина. ${loserName} побежден(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
} else if (loserCharacterKey === 'elena') {
|
||
this.addToLog(`Свет погас. ${loserName} повержен(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
}
|
||
|
||
console.log(`[Game ${this.id}] Game is over. Winner: ${winnerName} (${winnerRole}). Loser: ${loserName} (${loserRole}). Reason: HP <= 0.`);
|
||
|
||
// Отправляем событие конца игры всем клиентам в комнате
|
||
this.io.to(this.id).emit('gameOver', {
|
||
// В AI режиме winnerId отправляем только если победил игрок, иначе null
|
||
winnerId: this.mode === 'ai' ? (winnerRole === GAME_CONFIG.PLAYER_ID ? winnerRole : null) : winnerRole,
|
||
reason: `${loserName} побежден(а)`, // Причина для отображения на клиенте
|
||
finalGameState: this.gameState, // Финальное состояние игры для отображения
|
||
log: this.consumeLogBuffer(), // Отправляем весь накопленный лог (включая финальные сообщения)
|
||
loserCharacterKey: loserCharacterKey // Передаем characterKey проигравшего для клиентской анимации
|
||
});
|
||
|
||
// УВЕДОМЛЯЕМ GAMEMANAGER ОБ ОКОНЧАНИИ ИГРЫ ДЛЯ ОЧИСТКИ
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'hp_zero');
|
||
} else {
|
||
console.error(`[Game ${this.id}] GameManager reference missing or _cleanupGame not found! Game state will not be cleaned.`);
|
||
}
|
||
|
||
|
||
return true; // Игра окончена
|
||
}
|
||
|
||
// Если isOver было false, или gameState был некорректен, игра не окончена
|
||
return isOver;
|
||
}
|
||
|
||
/**
|
||
* Завершает игру из-за отключения одного из игроков.
|
||
* Вызывается GameManager.
|
||
* @param {string} disconnectedSocketId - socketId отключившегося игрока.
|
||
* @param {string} disconnectedPlayerRole - Роль ('player' или 'opponent') отключившегося игрока.
|
||
* @param {string} disconnectedCharacterKey - Ключ персонажа отключившегося игрока.
|
||
*/
|
||
endGameDueToDisconnect(disconnectedSocketId, disconnectedPlayerRole, disconnectedCharacterKey) {
|
||
if (this.gameState && !this.gameState.isGameOver) {
|
||
this.gameState.isGameOver = true; // Помечаем игру как оконченную
|
||
|
||
// Победитель - тот, кто остался. Его роль противоположна роли отключившегося.
|
||
const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const disconnectedCharacterData = this._getCharacterBaseData(disconnectedCharacterKey); // Данные уходящего персонажа
|
||
// Ключ персонажа победителя берем из gameState, т.к. там актуальное состояние слотов
|
||
const winnerCharacterKey = (winnerRole === GAME_CONFIG.PLAYER_ID) ? this.playerCharacterKey : this.opponentCharacterKey;
|
||
const winnerCharacterData = this._getCharacterBaseData(winnerCharacterKey); // Данные оставшегося персонажа
|
||
|
||
// Добавляем сообщение о дисконнекте в лог
|
||
this.addToLog(`🔌 Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} (${disconnectedPlayerRole}) отключился. Игра завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
// В AI режиме, если игрок отключился, AI не "побеждает" в стандартном смысле. Можно сделать специфичный лог.
|
||
if (this.mode === 'pvp') {
|
||
this.addToLog(`🏁 Победа присуждается ${winnerCharacterData?.name || winnerRole}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||
} else { // AI режим
|
||
// AI игра завершается без формальной победы AI, если игрок ушел
|
||
// Лог выше уже достаточен
|
||
}
|
||
|
||
|
||
// Отправляем событие конца игры всем клиентам в комнате
|
||
// GameManager отвечает за удаление игры и очистку ссылок userIdentifierToGameId.
|
||
// Здесь мы только уведомляем клиентов.
|
||
this.io.to(this.id).emit('gameOver', {
|
||
// В AI режиме winnerId отправляем null, т.к. нет формального победителя
|
||
winnerId: this.mode === 'pvp' ? winnerRole : null,
|
||
reason: 'opponent_disconnected', // Причина для клиента
|
||
finalGameState: this.gameState, // Финальное состояние игры для отображения
|
||
log: this.consumeLogBuffer(), // Отправляем весь накопленный лог
|
||
// Передаем characterKey проигравшего (того, кто отключился) для клиентской анимации (растворения)
|
||
loserCharacterKey: disconnectedCharacterKey
|
||
});
|
||
|
||
console.log(`[Game ${this.id}] Game ended due to disconnect. Winner (PvP only): ${winnerCharacterData?.name || winnerRole}. Disconnected: ${disconnectedCharacterData?.name || disconnectedPlayerRole}.`);
|
||
|
||
// УВЕДОМЛЯЕМ GAMEMANAGER ОБ ОКОНЧАНИИ ИГРЫ ДЛЯ ОЧИСТКИ
|
||
if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') {
|
||
this.gameManager._cleanupGame(this.id, 'opponent_disconnected');
|
||
} else {
|
||
console.error(`[Game ${this.id}] GameManager reference missing or _cleanupGame not found! Game state will not be cleaned.`);
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Добавляет сообщение в буфер лога игры.
|
||
* @param {string} message - Текст сообщения.
|
||
* @param {string} [type=GAME_CONFIG.LOG_TYPE_INFO] - Тип сообщения (для стилей на клиенте).
|
||
*/
|
||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||
if (!message) return;
|
||
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
||
// В активной игре можно сразу отправлять лог, не дожидаясь gameStateUpdate
|
||
// Это может улучшить отзывчивость лога.
|
||
// if (this.gameState && !this.gameState.isGameOver) {
|
||
// this.broadcastLogUpdate();
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* Очищает буфер лога и возвращает его содержимое.
|
||
* @returns {Array<object>} Массив сообщений лога.
|
||
*/
|
||
consumeLogBuffer() {
|
||
const logs = [...this.logBuffer];
|
||
this.logBuffer = [];
|
||
return logs;
|
||
}
|
||
|
||
/**
|
||
* Отправляет текущее состояние игры и содержимое лога всем клиентам в комнате игры.
|
||
*/
|
||
broadcastGameStateUpdate() {
|
||
if (!this.gameState) return;
|
||
// Отправляем полное состояние игры и буфер лога всем клиентам в комнате
|
||
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
|
||
}
|
||
|
||
/**
|
||
* Отправляет только содержимое лога всем клиентам в комнате игры (если нужно обновить только лог).
|
||
* Полезно для сообщений, которые не меняют gameState, но должны отобразиться.
|
||
*/
|
||
broadcastLogUpdate() {
|
||
if (this.logBuffer.length > 0) {
|
||
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
|
||
}
|
||
}
|
||
|
||
// --- Вспомогательные функции для получения данных персонажа из data.js ---
|
||
// Скопировано из gameManager.js, т.к. GameInstance тоже использует gameData напрямую
|
||
/**
|
||
* Получает базовые статы и список способностей для персонажа по ключу.
|
||
* Эти функции предназначены для использования ВНУТРИ GameManager или GameInstance.
|
||
* @param {string} key - Ключ персонажа ('elena', 'balard', 'almagest').
|
||
* @returns {{baseStats: object, abilities: array}|null} Объект с базовыми статами и способностями, или null.
|
||
*/
|
||
_getCharacterData(key) {
|
||
if (!key) { console.warn("GameInstance::_getCharacterData called with null/undefined key."); return null; }
|
||
switch (key) {
|
||
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
|
||
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; // Балард использует opponentAbilities из data.js
|
||
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; // Альмагест использует almagestAbilities из data.js
|
||
default: console.error(`GameInstance::_getCharacterData: Unknown character key "${key}"`); return null;
|
||
}
|
||
}
|
||
/**
|
||
* Получает только базовые статы для персонажа по ключу.
|
||
* @param {string} key - Ключ персонажа.
|
||
* @returns {object|null} Базовые статы или null.
|
||
*/
|
||
_getCharacterBaseData(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.baseStats : null;
|
||
}
|
||
/**
|
||
* Получает только список способностей для персонажа по ключу.
|
||
* @param {string} key - Ключ персонажа.
|
||
* @returns {array|null} Список способностей или null.
|
||
*/
|
||
_getCharacterAbilities(key) {
|
||
const charData = this._getCharacterData(key);
|
||
return charData ? charData.abilities : null;
|
||
}
|
||
}
|
||
|
||
module.exports = GameInstance; |