bc/server_modules/gameInstance.js

639 lines
45 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/gameInstance.js
const GAME_CONFIG = require('./config');
const gameData = require('./data');
const serverGameLogic = require('./gameLogic');
class GameInstance {
constructor(gameId, io, mode = 'ai') {
this.id = gameId;
this.io = io;
this.mode = mode; // 'ai' или 'pvp'
this.players = {}; // { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest' } }
this.playerSockets = {}; // { 'player': socketObject, 'opponent': socketObject } - для быстрого доступа к сокету по роли
this.playerCount = 0;
this.gameState = null; // Хранит текущее состояние игры (HP, ресурсы, эффекты, чей ход и т.д.)
this.aiOpponent = (mode === 'ai');
this.logBuffer = []; // Буфер для сообщений лога боя
// this.restartVotes = new Set(); // Удалено, так как рестарт той же сессии убран
// Ключи персонажей для текущей игры
this.playerCharacterKey = null; // Ключ персонажа в слоте 'player' (Елена или Альмагест)
this.opponentCharacterKey = null; // Ключ персонажа в слоте 'opponent' (Балард, Елена или Альмагест)
this.ownerUserId = null; // userId создателя игры (важно для PvP ожидающих игр)
}
addPlayer(socket, chosenCharacterKey = 'elena') {
if (this.players[socket.id]) {
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' });
console.warn(`[Game ${this.id}] Игрок ${socket.id} попытался присоединиться к игре, в которой уже состоит.`);
return false;
}
if (this.playerCount >= 2) {
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
return false;
}
let assignedPlayerId; // 'player' или 'opponent' (технический ID слота)
let actualCharacterKey; // 'elena', 'almagest', 'balard'
if (this.mode === 'ai') {
if (this.playerCount > 0) {
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
return false;
}
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = 'elena'; // В AI режиме игрок всегда Елена
if (socket.userData?.userId) {
this.ownerUserId = socket.userData.userId; // Запоминаем создателя
}
} else { // PvP режим
if (this.playerCount === 0) { // Первый игрок в PvP
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
if (socket.userData?.userId) {
this.ownerUserId = socket.userData.userId; // Запоминаем создателя
}
} else { // Второй игрок в PvP
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
const firstPlayerInfo = Object.values(this.players)[0];
// Второй игрок автоматически получает "зеркального" персонажа
actualCharacterKey = (firstPlayerInfo.chosenCharacterKey === 'elena') ? 'almagest' : 'elena';
}
}
this.players[socket.id] = {
id: assignedPlayerId,
socket: socket,
chosenCharacterKey: actualCharacterKey // Запоминаем ключ выбранного/назначенного персонажа
};
this.playerSockets[assignedPlayerId] = socket;
this.playerCount++;
socket.join(this.id); // Присоединяем сокет к комнате игры
const characterData = this._getCharacterBaseData(actualCharacterKey);
console.log(`[Game ${this.id}] Игрок ${socket.userData?.username || socket.id} (userId: ${socket.userData?.userId || 'N/A'}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerUserId || 'N/A'}`);
if (this.mode === 'pvp' && this.playerCount < 2) {
socket.emit('waitingForOpponent');
}
// Если игра готова к старту (2 игрока в PvP, или 1 в AI)
if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) {
this.initializeGame(); // Инициализируем состояние игры
if (this.gameState) {
this.startGame(); // Запускаем игру
} else {
console.error(`[Game ${this.id}] Не удалось запустить игру: gameState не был инициализирован.`);
// Ошибка должна была быть отправлена клиенту из initializeGame
}
}
return true;
}
removePlayer(socketId) {
const playerInfo = this.players[socketId];
if (playerInfo) {
const playerRole = playerInfo.id; // 'player' or 'opponent'
let characterKeyOfLeavingPlayer = playerInfo.chosenCharacterKey;
const userIdOfLeavingPlayer = playerInfo.socket?.userData?.userId;
const usernameOfLeavingPlayer = playerInfo.socket?.userData?.username || socketId;
// Для AI оппонента, у него нет записи в this.players, но его ключ 'balard'
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) { // Если уходит игрок из AI игры
// AI оппонент не имеет 'chosenCharacterKey' в this.players, так как он не сокет
} else if (!characterKeyOfLeavingPlayer && this.gameState) {
// Фоллбэк, если ключ не был в playerInfo (маловероятно для реальных игроков)
characterKeyOfLeavingPlayer = (playerRole === GAME_CONFIG.PLAYER_ID)
? this.gameState.player?.characterKey
: this.gameState.opponent?.characterKey;
}
const characterData = this._getCharacterBaseData(characterKeyOfLeavingPlayer);
console.log(`[Game ${this.id}] Игрок ${usernameOfLeavingPlayer} (socket: ${socketId}, userId: ${userIdOfLeavingPlayer || 'N/A'}) (${characterData?.name || 'Неизвестно'}, роль: ${playerRole}, персонаж: ${characterKeyOfLeavingPlayer || 'N/A'}) покинул игру.`);
if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) {
delete this.playerSockets[playerRole];
}
delete this.players[socketId];
this.playerCount--;
// Если создатель PvP игры вышел, и остался один игрок, обновляем ownerUserId
if (this.mode === 'pvp' && this.ownerUserId === userIdOfLeavingPlayer && this.playerCount === 1) {
const remainingPlayerSocketId = Object.keys(this.players)[0];
const remainingPlayerSocket = this.players[remainingPlayerSocketId]?.socket;
this.ownerUserId = remainingPlayerSocket?.userData?.userId || null; // Новый владелец - userId оставшегося или null
console.log(`[Game ${this.id}] Owner left PvP game. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`);
} else if (this.playerCount === 0) {
this.ownerUserId = null; // Если игра пуста, нет владельца
}
// Если игра была активна, завершаем ее из-за дисконнекта
if (this.gameState && !this.gameState.isGameOver) {
this.endGameDueToDisconnect(playerRole, characterKeyOfLeavingPlayer || (playerRole === GAME_CONFIG.PLAYER_ID ? this.playerCharacterKey : this.opponentCharacterKey) );
}
}
}
endGameDueToDisconnect(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);
const winnerCharacterKey = (winnerRole === GAME_CONFIG.PLAYER_ID) ? this.playerCharacterKey : this.opponentCharacterKey;
const winnerCharacterData = this._getCharacterBaseData(winnerCharacterKey);
this.addToLog(`Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} покинул игру. Победа присуждается ${winnerCharacterData?.name || winnerRole}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('opponentDisconnected', { disconnectedPlayerId: disconnectedPlayerRole, disconnectedCharacterName: disconnectedCharacterData?.name });
this.io.to(this.id).emit('gameOver', {
winnerId: winnerRole,
reason: 'opponent_disconnected',
finalGameState: this.gameState,
log: this.consumeLogBuffer()
});
}
}
initializeGame() {
console.log(`[Game ${this.id}] Initializing game state for (re)start... Mode: ${this.mode}`);
if (this.mode === 'ai') {
this.playerCharacterKey = 'elena'; // Игрок в AI всегда Елена
this.opponentCharacterKey = 'balard'; // AI всегда Балард
} else { // pvp
const playerSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
const opponentSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Фоллбэк, если что-то пошло не так
if (this.playerCount === 2 && opponentSocketInfo) {
this.opponentCharacterKey = opponentSocketInfo.chosenCharacterKey;
// Дополнительная проверка, чтобы персонажи были разными, если вдруг оба выбрали одного
if (this.playerCharacterKey === this.opponentCharacterKey) {
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
// Обновляем ключ у второго игрока, если он был изменен
if (opponentSocketInfo.chosenCharacterKey !== this.opponentCharacterKey) {
opponentSocketInfo.chosenCharacterKey = this.opponentCharacterKey;
console.warn(`[Game ${this.id}] PvP character conflict resolved. Opponent in slot '${GAME_CONFIG.OPPONENT_ID}' is now ${this.opponentCharacterKey}.`);
}
}
} else if (this.playerCount === 1) { // Только один игрок в PvP, оппонент еще не определен
this.opponentCharacterKey = null;
} else { // Неожиданная ситуация
console.error(`[Game ${this.id}] Unexpected playerCount (${this.playerCount}) or missing socketInfo during PvP character key assignment.`);
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; // Аварийный фоллбэк
}
}
console.log(`[Game ${this.id}] Finalizing characters - 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 игра)
if (this.opponentCharacterKey) {
opponentBase = this._getCharacterBaseData(this.opponentCharacterKey);
opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey);
}
// Проверяем, готовы ли мы к созданию полного игрового состояния
const isReadyForFullGameState = (this.mode === 'ai') || (this.mode === 'pvp' && this.playerCount === 2 && opponentBase && opponentAbilities);
if (!playerBase || !playerAbilities || (!isReadyForFullGameState && !(this.mode === 'pvp' && this.playerCount === 1))) {
console.error(`[Game ${this.id}] CRITICAL ERROR: Failed to load necessary character data for initialization! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}, PlayerCount: ${this.playerCount}, Mode: ${this.mode}`);
this.logBuffer = []; // Очищаем лог
this.addToLog('Критическая ошибка сервера при инициализации персонажей!', GAME_CONFIG.LOG_TYPE_SYSTEM);
// Уведомляем игроков в комнате об ошибке
this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры. Не удалось загрузить данные персонажей.' });
this.gameState = null; // Не создаем gameState
return;
}
// Создаем gameState
this.gameState = {
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: { // Данные оппонента, если он есть, иначе плейсхолдеры
id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey,
name: opponentBase?.name || 'Ожидание игрока...',
currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1,
currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0,
resourceName: opponentBase?.resourceName || 'Неизвестно', attackPower: opponentBase?.attackPower || 0,
isBlocking: false, activeEffects: [],
// Специальные кулдауны для Баларда (AI)
silenceCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
manaDrainCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
abilityCooldowns: {}
},
isPlayerTurn: Math.random() < 0.5, // Случайный первый ход
isGameOver: false,
turnNumber: 1,
gameMode: this.mode
};
// Инициализация кулдаунов способностей
playerAbilities.forEach(ability => {
if (typeof ability.cooldown === 'number' && ability.cooldown > 0) {
this.gameState.player.abilityCooldowns[ability.id] = 0;
}
});
if (opponentAbilities) {
opponentAbilities.forEach(ability => {
let cd = 0;
if (ability.cooldown) cd = ability.cooldown;
else if (this.opponentCharacterKey === 'balard') { // Специальные внутренние КД для AI Баларда
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) {
cd = GAME_CONFIG[ability.internalCooldownFromConfig];
} else if (typeof ability.internalCooldownValue === 'number') {
cd = ability.internalCooldownValue;
}
}
if (cd > 0) {
this.gameState.opponent.abilityCooldowns[ability.id] = 0;
}
});
}
const isRestart = this.logBuffer.length > 0 && isReadyForFullGameState; // Проверяем, был ли лог до этого (признак рестарта)
this.logBuffer = []; // Очищаем лог перед новой игрой/рестартом
if (isReadyForFullGameState) { // Лог о начале битвы только если игра полностью готова
this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
}
console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState.isGameOver}. First turn: ${this.gameState.isPlayerTurn ? this.gameState.player.name : (this.gameState.opponent?.name || 'Оппонент')}`);
}
startGame() {
// Проверяем, что игра полностью готова к запуску (оба игрока есть и gameState инициализирован)
if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание игрока...') {
if (this.mode === 'pvp' && this.playerCount === 1 && !this.opponentCharacterKey) {
console.log(`[Game ${this.id}] startGame: PvP игра ожидает второго игрока.`);
} else if (!this.gameState) {
console.error(`[Game ${this.id}] Game cannot start: gameState is null.`);
} else {
console.warn(`[Game ${this.id}] Game not fully ready to start. OpponentKey: ${this.opponentCharacterKey}, OpponentName: ${this.gameState.opponent?.name}, PlayerCount: ${this.playerCount}`);
}
return;
}
console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver: ${this.gameState.isGameOver}`);
const playerCharData = this._getCharacterData(this.playerCharacterKey);
const opponentCharData = this._getCharacterData(this.opponentCharacterKey);
if (!playerCharData || !opponentCharData) {
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: 'Критическая ошибка сервера при старте игры (не удалось загрузить данные персонажей).' });
return;
}
// Отправляем каждому игроку его персональные данные для игры
Object.values(this.players).forEach(playerInfo => {
let dataForThisClient;
if (playerInfo.id === GAME_CONFIG.PLAYER_ID) { // Этот клиент играет за слот 'player'
dataForThisClient = {
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
playerBaseStats: playerCharData.baseStats, opponentBaseStats: opponentCharData.baseStats,
playerAbilities: playerCharData.abilities, opponentAbilities: opponentCharData.abilities,
log: this.consumeLogBuffer(), // Первый игрок получает весь накопленный лог
clientConfig: { ...GAME_CONFIG } // Копия конфига для клиента
};
} else { // Этот клиент играет за слот 'opponent'
dataForThisClient = {
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
// Меняем местами статы и абилки, чтобы клиент видел себя как 'player', а противника как 'opponent'
playerBaseStats: opponentCharData.baseStats, opponentBaseStats: playerCharData.baseStats,
playerAbilities: opponentCharData.abilities, opponentAbilities: playerCharData.abilities,
log: [], // Второй игрок не получает стартовый лог, чтобы избежать дублирования
clientConfig: { ...GAME_CONFIG }
};
}
playerInfo.socket.emit('gameStarted', dataForThisClient);
});
const firstTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name;
this.addToLog(`--- ${firstTurnName} ходит первым! (Ход ${this.gameState.turnNumber}) ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate(); // Отправляем начальное состояние и лог
// Если ход AI, запускаем его логику
if (!this.gameState.isPlayerTurn) {
if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else { // PvP, ход второго игрока
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
}
} else { // Ход первого игрока (реального)
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
}
}
// Метод handleVoteRestart удален
processPlayerAction(requestingSocketId, actionData) {
if (!this.gameState || this.gameState.isGameOver) return;
const actingPlayerInfo = this.players[requestingSocketId];
if (!actingPlayerInfo) { console.error(`[Game ${this.id}] Action from unknown socket ${requestingSocketId}`); return; }
const actingPlayerRole = actingPlayerInfo.id; // 'player' или 'opponent'
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
(!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID);
if (!isCorrectTurn) {
actingPlayerInfo.socket.emit('gameError', { message: "Сейчас не ваш ход!" });
return;
}
const attackerState = this.gameState[actingPlayerRole];
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const defenderState = this.gameState[defenderRole];
const attackerData = this._getCharacterData(attackerState.characterKey);
const defenderData = this._getCharacterData(defenderState.characterKey);
if (!attackerData || !defenderData) {
this.addToLog('Критическая ошибка сервера при обработке действия (не найдены данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate(); return;
}
let actionValid = true; // Флаг валидности действия
// Обработка атаки
if (actionData.actionType === 'attack') {
serverGameLogic.performAttack(
attackerState, defenderState, attackerData.baseStats, defenderData.baseStats,
this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData
);
// Логика для "Силы Природы" и аналогов - бафф применяется после атаки
const attackBuffAbilityId = attackerState.characterKey === 'elena' ? GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH
: (attackerState.characterKey === 'almagest' ? GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK : null);
if (attackBuffAbilityId) {
const attackBuffEffect = attackerState.activeEffects.find(eff => eff.id === attackBuffAbilityId);
if (attackBuffEffect && !attackBuffEffect.justCast) { // Эффект должен быть активен и не только что применен
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerData.baseStats.maxResource - attackerState.currentResource);
if (actualRegen > 0) {
attackerState.currentResource += actualRegen;
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${attackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
}
// Не удаляем эффект, если он многоразовый. Если одноразовый - удалить тут.
// В текущей реализации Сила Природы имеет duration, поэтому управляется через processEffects.
}
}
// Обработка способности
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
if (!ability) { actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." }); return; }
// Проверки валидности использования способности
if (attackerState.currentResource < ability.cost) { this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
if (actionValid && attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) { this.addToLog(`"${ability.name}" еще на перезарядке (${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
// Специальные КД для Баларда
if (actionValid && attackerState.characterKey === 'balard') {
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) { this.addToLog(`"${ability.name}" еще не готова (спец. КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0) { this.addToLog(`"${ability.name}" еще не готова (спец. КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
}
// Нельзя кастовать бафф, если он уже активен
if (actionValid && ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id)) { this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
const isTargetedDebuff = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF;
if (actionValid && isTargetedDebuff) {
if (defenderState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { // Ищем эффект с префиксом effect_
this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
}
if (actionValid) {
attackerState.currentResource -= ability.cost;
// Установка кулдауна
let baseCooldown = 0;
if (ability.cooldown) baseCooldown = ability.cooldown;
else if (attackerState.characterKey === 'balard') { // Специальные внутренние КД для AI
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { 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 && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; }
else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; }
}
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1; // +1, т.к. уменьшится в конце этого хода
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
} else { actionValid = false; } // Неизвестный тип действия
if (!actionValid) { this.broadcastLogUpdate(); return; } // Если действие невалидно, просто отправляем лог и выходим
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем конец игры после действия
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); // Переключаем ход с задержкой
}
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(`SwitchTurn Error: No char data for ${endingTurnActorState.characterKey}`); return; }
// Обработка эффектов в конце хода (DoT, HoT, истечение баффов/дебаффов)
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) {
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 (endingTurnActorRole === GAME_CONFIG.OPPONENT_ID) { // Если это был ход оппонента (AI или PvP)
const playerStateInGame = this.gameState.player; // Игрок, на которого могло быть наложено безмолвие
if (playerStateInGame.disabledAbilities?.length > 0) {
const playerCharAbilities = this._getCharacterAbilities(playerStateInGame.characterKey);
if (playerCharAbilities) serverGameLogic.processDisabledAbilities(playerStateInGame.disabledAbilities, playerCharAbilities, playerStateInGame.name, this.addToLog.bind(this));
}
}
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем конец игры после эффектов
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; // Меняем ход
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, запускаем его логику
if (!this.gameState.isPlayerTurn) {
if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else { // PvP, ход второго игрока
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
}
} else { // Ход первого игрока
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
}
}
processAiTurn() {
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.opponentCharacterKey !== 'balard') {
if(!this.gameState || this.gameState.isGameOver) return; // Если игра закончена, ничего не делаем
// Если не ход AI или это не AI Балард, выходим (хотя эта проверка должна быть раньше)
return;
}
const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this));
const attackerState = this.gameState.opponent; // AI всегда в слоте 'opponent' в AI режиме
const defenderState = this.gameState.player;
const attackerData = this._getCharacterData('balard');
const defenderData = this._getCharacterData(defenderState.characterKey); // Обычно 'elena'
if (!attackerData || !defenderData) { this.addToLog("AI не может действовать: ошибка данных персонажа.", GAME_CONFIG.LOG_TYPE_SYSTEM); this.switchTurn(); return; }
let actionValid = true;
if (aiDecision.actionType === 'attack') {
// Лог атаки уже будет в performAttack
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
const ability = aiDecision.ability;
// Проверки валидности (ресурс, КД) для AI
if (attackerState.currentResource < ability.cost ||
(attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0)
) {
actionValid = false;
this.addToLog(`AI ${attackerState.name} не смог применить "${ability.name}" (недостаточно ресурса или на перезарядке). Решил атаковать.`, GAME_CONFIG.LOG_TYPE_INFO);
// Если выбранная способность невалидна, AI по умолчанию атакует
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
if (actionValid) { // Если способность все еще валидна
attackerState.currentResource -= ability.cost;
// Установка кулдауна для AI
let baseCooldown = 0;
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { 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 && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue;}
else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; }
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
} 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 или ошибка
actionValid = false;
this.addToLog(`AI ${attackerState.name} не смог выбрать действие и атакует.`, GAME_CONFIG.LOG_TYPE_INFO);
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
// if (!actionValid && aiDecision.actionType !== 'pass') {
// this.addToLog(`${attackerState.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
// }
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
this.switchTurn(); // Переключаем ход после действия AI
}
checkGameOver() {
if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true; // Если игра уже закончена, или нет gameState
const playerState = this.gameState.player;
const opponentState = this.gameState.opponent;
if (!playerState || !opponentState || opponentState.name === 'Ожидание игрока...') {
// Если одного из игроков нет (например, PvP игра ожидает второго), игра не может закончиться по HP
return false;
}
const playerDead = playerState.currentHp <= 0;
const opponentDead = opponentState.currentHp <= 0;
if (playerDead || opponentDead) {
this.gameState.isGameOver = true;
const winnerRole = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const loserRole = opponentDead ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
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 ? "Игрок" : "Противник");
this.addToLog(`🏁 ПОБЕДА! ${winnerName} одолел(а) ${loserName}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
// Дополнительные сообщения о конце игры
if (winnerState?.characterKey === 'elena') {
const tauntContext = loserState?.characterKey === 'balard' ? 'opponentNearDefeatBalard' : 'opponentNearDefeatAlmagest';
const taunt = serverGameLogic.getElenaTaunt(tauntContext, {}, GAME_CONFIG, gameData, this.gameState);
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
if (loserState?.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
else if (loserState?.characterKey === 'almagest') this.addToLog(`Елена одержала победу над темной волшебницей ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
}
this.io.to(this.id).emit('gameOver', {
winnerId: winnerRole,
reason: `${loserName} побежден(а)`,
finalGameState: this.gameState,
log: this.consumeLogBuffer()
});
return true;
}
return false;
}
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
if (!message) return;
this.logBuffer.push({ message, type, timestamp: Date.now() });
}
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() });
}
broadcastLogUpdate() { // Если нужно отправить только лог без полного gameState
if (this.logBuffer.length > 0) {
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
}
}
// Вспомогательные функции для получения данных персонажа
_getCharacterData(key) {
if (!key) return null;
switch (key) {
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities };
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities };
default: console.error(`_getCharacterData: Unknown character key "${key}"`); return null;
}
}
_getCharacterBaseData(key) {
if (!key) return null;
const charData = this._getCharacterData(key);
return charData ? charData.baseStats : null;
}
_getCharacterAbilities(key) {
if (!key) return null;
const charData = this._getCharacterData(key);
return charData ? charData.abilities : null;
}
}
module.exports = GameInstance;