639 lines
45 KiB
JavaScript
639 lines
45 KiB
JavaScript
// /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; |