// /server_modules/gameInstance.js const { v4: uuidv4 } = require('uuid'); 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.players = {}; this.playerSockets = {}; this.playerCount = 0; this.mode = mode; this.gameState = null; this.aiOpponent = (mode === 'ai'); this.logBuffer = []; this.restartVotes = new Set(); } addPlayer(socket) { // ... (код без изменений) if (this.playerCount >= 2) { socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); return false; } let assignedPlayerId; let characterName; if (this.playerCount === 0) { assignedPlayerId = GAME_CONFIG.PLAYER_ID; characterName = gameData.playerBaseStats.name; } else { if (this.mode === 'ai') { socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); return false; } assignedPlayerId = GAME_CONFIG.OPPONENT_ID; characterName = gameData.opponentBaseStats.name; } this.players[socket.id] = { id: assignedPlayerId, socket: socket, characterName: characterName }; this.playerSockets[assignedPlayerId] = socket; this.playerCount++; socket.join(this.id); console.log(`[Game ${this.id}] Игрок ${socket.id} (${characterName}) присоединился как ${assignedPlayerId}.`); if (this.mode === 'pvp' && this.playerCount < 2) { socket.emit('waitingForOpponent'); } if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) { this.initializeGame(); this.startGame(); } return true; } removePlayer(socketId) { // ... (код без изменений) const playerInfo = this.players[socketId]; if (playerInfo) { const playerRole = playerInfo.id; console.log(`[Game ${this.id}] Игрок ${socketId} (${playerInfo.characterName}) покинул игру.`); if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) { delete this.playerSockets[playerRole]; } delete this.players[socketId]; this.playerCount--; if (this.gameState && !this.gameState.isGameOver) { this.endGameDueToDisconnect(playerRole); } } } endGameDueToDisconnect(disconnectedPlayerRole) { // ... (код без изменений) 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 disconnectedName = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats.name : gameData.opponentBaseStats.name; this.addToLog(`Игрок ${disconnectedName} покинул игру.`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.io.to(this.id).emit('gameOver', { winnerId: this.mode === 'pvp' ? winnerRole : GAME_CONFIG.OPPONENT_ID, reason: 'opponent_disconnected', finalGameState: this.gameState, log: this.consumeLogBuffer() }); } } initializeGame() { console.log(`[Game ${this.id}] Initializing game state for (re)start...`); const playerBase = gameData.playerBaseStats; const opponentBase = gameData.opponentBaseStats; this.gameState = { player: { id: GAME_CONFIG.PLAYER_ID, 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, name: opponentBase.name, currentHp: opponentBase.maxHp, maxHp: opponentBase.maxHp, currentResource: opponentBase.maxResource, maxResource: opponentBase.maxResource, resourceName: opponentBase.resourceName, attackPower: opponentBase.attackPower, isBlocking: false, activeEffects: [], silenceCooldownTurns: 0, manaDrainCooldownTurns: 0, abilityCooldowns: {} }, isPlayerTurn: Math.random() < 0.5, isGameOver: false, // Очень важно для рестарта! turnNumber: 1, gameMode: this.mode }; gameData.playerAbilities.forEach(ability => { if (typeof ability.cooldown === 'number') { this.gameState.player.abilityCooldowns[ability.id] = 0; } }); gameData.opponentAbilities.forEach(ability => { let cd = 0; // For opponent abilities, specific cooldowns like silenceCooldownTurns are often handled separately. // The general abilityCooldowns map can still be used for other abilities or as a backup. if (ability.cooldown) { // General cooldown field (if you add it to data.js for opponent abilities) cd = ability.cooldown; } else if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) { cd = GAME_CONFIG[ability.internalCooldownFromConfig]; } else if (typeof ability.internalCooldownValue === 'number') { cd = ability.internalCooldownValue; } // Initialize general cooldown to 0 (ready) for abilities that have a cooldown value. // Specific cooldowns like silenceCooldownTurns are initialized separately. if (cd > 0) { this.gameState.opponent.abilityCooldowns[ability.id] = 0; } }); // Specific cooldowns for Balard - ensuring they are reset this.gameState.opponent.silenceCooldownTurns = 0; this.gameState.opponent.manaDrainCooldownTurns = 0; this.restartVotes.clear(); const isRestart = this.logBuffer.length > 0; // Если лог не пуст, вероятно это рестарт this.logBuffer = []; 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 ? 'Елена' : 'Балард'}`); } startGame() { console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver should be false: ${this.gameState.isGameOver}`); Object.values(this.players).forEach(pInfo => { pInfo.socket.emit('gameStarted', { gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState, // Отправляем свежеинициализированное состояние playerBaseStats: gameData.playerBaseStats, opponentBaseStats: gameData.opponentBaseStats, playerAbilities: gameData.playerAbilities, opponentAbilities: gameData.opponentAbilities, log: this.consumeLogBuffer(), // Отправляем лог clientConfig: { ...GAME_CONFIG } // Отправляем полный GAME_CONFIG }); }); const firstTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name; this.addToLog(`--- ${firstTurnName} ходит первым! ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); // Отправляем состояние с первым логом хода if (!this.gameState.isPlayerTurn) { if (this.aiOpponent) { console.log(`[Game ${this.id}] AI's turn. Scheduling AI action.`); setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } else { console.log(`[Game ${this.id}] Opponent's turn (PvP). Notifying client.`); this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID }); } } else { console.log(`[Game ${this.id}] Player's turn (Елена). Notifying client.`); this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID }); } } handleVoteRestart(requestingSocketId) { console.log(`[Game ${this.id}] handleVoteRestart called by ${requestingSocketId}. isGameOver: ${this.gameState?.isGameOver}`); if (!this.gameState || !this.gameState.isGameOver) { console.warn(`[Game ${this.id}] Vote restart rejected: game not over or no game state.`); const playerSocket = this.io.sockets.sockets.get(requestingSocketId); if(playerSocket) playerSocket.emit('gameError', {message: "Нельзя рестартовать игру, которая не завершена."}); return; } if (!this.players[requestingSocketId]) { console.warn(`[Game ${this.id}] Vote restart rejected: unknown player ${requestingSocketId}.`); return; } this.restartVotes.add(requestingSocketId); const voterInfo = this.players[requestingSocketId]; this.addToLog(`Игрок ${voterInfo.characterName} (${voterInfo.id}) голосует за рестарт.`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.broadcastLogUpdate(); // Для PvP, requiredVotes должен быть равен текущему количеству игроков в комнате. // Если один игрок, то 1 голос. Если два, то 2 голоса. const requiredVotes = this.playerCount > 0 ? this.playerCount : 1; console.log(`[Game ${this.id}] Votes for restart: ${this.restartVotes.size}/${requiredVotes}. Players in game: ${this.playerCount}`); if (this.restartVotes.size >= requiredVotes) { console.log(`[Game ${this.id}] All players voted for restart or single player restart. Restarting game...`); this.initializeGame(); this.startGame(); } else if (this.mode === 'pvp') { // Отправляем уведомление о голосовании только в PvP, если не все проголосовали this.io.to(this.id).emit('waitingForRestartVote', { voterCharacterName: voterInfo.characterName, voterRole: voterInfo.id, votesNeeded: requiredVotes - this.restartVotes.size }); } } 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; 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 attackerBaseStats = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats : gameData.opponentBaseStats; const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const defenderState = this.gameState[defenderRole]; const defenderBaseStats = defenderRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats : gameData.opponentBaseStats; let actionValid = true; if (actionData.actionType === 'attack') { if (actingPlayerRole === GAME_CONFIG.PLAYER_ID) { const taunt = serverGameLogic.getElenaTaunt( 'playerBasicAttack', { opponentHpPerc: (defenderState.currentHp / defenderBaseStats.maxHp) * 100 }, GAME_CONFIG, gameData, this.gameState ); this.addToLog(`${attackerState.name} атакует: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); const natureEffectIndex = attackerState.activeEffects.findIndex( eff => eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH && !eff.justCast ); if (natureEffectIndex !== -1) { this.addToLog(`✨ Сила Природы активна! Атака восстановит ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_EFFECT); } } else { this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); } serverGameLogic.performAttack( attackerState, defenderState, attackerBaseStats, defenderBaseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG ); if (actingPlayerRole === GAME_CONFIG.PLAYER_ID) { const natureEffectIndex = attackerState.activeEffects.findIndex( eff => eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH && !eff.justCast ); if (natureEffectIndex !== -1) { const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerBaseStats.maxResource - attackerState.currentResource); if (actualRegen > 0) { attackerState.currentResource += actualRegen; this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от Силы Природы!`, GAME_CONFIG.LOG_TYPE_HEAL); } } } } else if (actionData.actionType === 'ability' && actionData.abilityId) { const abilityList = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? gameData.playerAbilities : gameData.opponentAbilities; const ability = abilityList.find(ab => ab.id === actionData.abilityId); if (!ability) { console.error(`[Game ${this.id}] Неизвестная способность ID: ${actionData.abilityId} для игрока ${actingPlayerRole}`); 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; } // Check general cooldowns if (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) { this.addToLog(`"${ability.name}" еще не готова (КД: ${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } // Check specific opponent cooldowns if this is the opponent if (actingPlayerRole === GAME_CONFIG.OPPONENT_ID) { if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) { this.addToLog(`"${ability.name}" еще не готова (спец. КД: ${attackerState.silenceCooldownTurns} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0) { this.addToLog(`"${ability.name}" еще не готова (спец. КД: ${attackerState.manaDrainCooldownTurns} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } } if (actionValid && actingPlayerRole === GAME_CONFIG.PLAYER_ID && ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS) { const effectIdForDebuff = 'effect_' + ability.id; if (defenderState.activeEffects.some(e => e.id === effectIdForDebuff)) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.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; } if (actionValid) { attackerState.currentResource -= ability.cost; // Set cooldowns let baseCooldown = 0; if (ability.cooldown) { // For player abilities mainly baseCooldown = ability.cooldown; } else if (actingPlayerRole === GAME_CONFIG.OPPONENT_ID) { // For Balard if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Set specific CD baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Also set general CD for consistency if needed elsewhere } else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; // Set specific CD baseCooldown = ability.internalCooldownValue; // Also set general CD } else { // For other potential Balard abilities with general CD 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 because it ticks down at end of current turn } let logMessage = `${attackerState.name} колдует "${ability.name}" (-${ability.cost} ${attackerState.resourceName})`; if (actingPlayerRole === GAME_CONFIG.PLAYER_ID) { const taunt = serverGameLogic.getElenaTaunt( 'playerActionCast', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState ); logMessage += `: "${taunt}"`; } let logType = ability.type === GAME_CONFIG.ACTION_TYPE_HEAL ? GAME_CONFIG.LOG_TYPE_HEAL : ability.type === GAME_CONFIG.ACTION_TYPE_DAMAGE ? GAME_CONFIG.LOG_TYPE_DAMAGE : GAME_CONFIG.LOG_TYPE_EFFECT; this.addToLog(logMessage, logType); const targetIdForAbility = ( ability.type === GAME_CONFIG.ACTION_TYPE_DAMAGE || ability.type === GAME_CONFIG.ACTION_TYPE_DEBUFF || ability.id === GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE || (actingPlayerRole === GAME_CONFIG.OPPONENT_ID && ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) || (actingPlayerRole === GAME_CONFIG.OPPONENT_ID && ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN) ) ? defenderRole : actingPlayerRole; const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF) ? attackerState : defenderState; const targetBaseStatsForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF) ? attackerBaseStats : defenderBaseStats; serverGameLogic.applyAbilityEffect( ability, attackerState, targetForAbility, // Corrected target state attackerBaseStats, targetBaseStatsForAbility, // Corrected target base stats this.gameState, this.addToLog.bind(this), GAME_CONFIG ); } } if (!actionValid) { this.broadcastLogUpdate(); return; } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Action was valid and processed, delay next step if needed 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 endingTurnActorBaseStats = endingTurnActorRole === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats : gameData.opponentBaseStats; serverGameLogic.processEffects( endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorBaseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG ); if (serverGameLogic.updateBlockingStatus) { serverGameLogic.updateBlockingStatus(this.gameState.player); serverGameLogic.updateBlockingStatus(this.gameState.opponent); } if (this.gameState.isPlayerTurn) { // Player (Elena) just finished her turn if (serverGameLogic.processPlayerAbilityCooldowns) { serverGameLogic.processPlayerAbilityCooldowns( this.gameState.player.abilityCooldowns, this.addToLog.bind(this), this.gameState.player.name, gameData.playerAbilities ); } } else { // Opponent (Balard) just finished his turn if (serverGameLogic.processDisabledAbilities) { // Process effects on Elena like silence serverGameLogic.processDisabledAbilities( this.gameState.player.disabledAbilities, this.addToLog.bind(this) ); } // Decrement Balard's specific cooldowns if (this.gameState.opponent.silenceCooldownTurns > 0) { this.gameState.opponent.silenceCooldownTurns--; } if (this.gameState.opponent.manaDrainCooldownTurns > 0) { this.gameState.opponent.manaDrainCooldownTurns--; } // Decrement Balard's general cooldowns if (this.gameState.opponent.abilityCooldowns) { serverGameLogic.processPlayerAbilityCooldowns( // Re-using this function for opponent this.gameState.opponent.abilityCooldowns, this.addToLog.bind(this), this.gameState.opponent.name, gameData.opponentAbilities ); } } if (this.checkGameOver()) { // Check game over after effects and cooldowns processed this.broadcastGameStateUpdate(); return; } this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; this.gameState.turnNumber++; const currentTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name; this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnName} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); if (!this.gameState.isPlayerTurn) { // If it's now Opponent's turn if (this.aiOpponent) { setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } else { // PvP opponent's turn this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID }); } } else { // If it's now Player's turn this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID }); } } processAiTurn() { // ... (код без изменений) if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) return; console.log(`[Game ${this.id}] AI Opponent (Балард) turn`); const aiDecision = serverGameLogic.decideAiAction( this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this) ); if (!aiDecision || aiDecision.actionType === 'pass') { if (aiDecision && aiDecision.logMessage) { this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type); } else { this.addToLog(`${this.gameState.opponent.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO); } this.switchTurn(); // Directly switch turn, no delay needed for AI's own pass return; } let actionProcessed = false; if (aiDecision.actionType === 'attack') { this.addToLog(`${this.gameState.opponent.name} атакует ${this.gameState.player.name}!`, GAME_CONFIG.LOG_TYPE_INFO); serverGameLogic.performAttack( this.gameState.opponent, this.gameState.player, gameData.opponentBaseStats, gameData.playerBaseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG ); actionProcessed = true; } else if (aiDecision.actionType === 'ability' && aiDecision.ability) { const ability = aiDecision.ability; // Double check resource and cooldowns here, though AI should already consider it if (this.gameState.opponent.currentResource < ability.cost) { this.addToLog(`${this.gameState.opponent.name} пытается применить "${ability.name}", но не хватает ${this.gameState.opponent.resourceName}! (AI Ошибка?)`, GAME_CONFIG.LOG_TYPE_INFO); } else if ((ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && this.gameState.opponent.silenceCooldownTurns > 0) || (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && this.gameState.opponent.manaDrainCooldownTurns > 0) || (this.gameState.opponent.abilityCooldowns && this.gameState.opponent.abilityCooldowns[ability.id] > 0)) { this.addToLog(`AI попытался использовать "${ability.name}" на кулдауне. (AI Ошибка?)`, GAME_CONFIG.LOG_TYPE_INFO); } else { this.gameState.opponent.currentResource -= ability.cost; // Set cooldowns let baseCooldown = 0; if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { this.gameState.opponent.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; // Also for general CD map } else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { this.gameState.opponent.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; // Also for general CD map } else { // For other abilities that might use the general map if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) { baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; } else if (typeof ability.internalCooldownValue === 'number') { baseCooldown = ability.internalCooldownValue; } } if (baseCooldown > 0 && this.gameState.opponent.abilityCooldowns) { // +1 because it ticks down at end of *this current* turn this.gameState.opponent.abilityCooldowns[ability.id] = baseCooldown +1; } this.addToLog(`${this.gameState.opponent.name} применяет "${ability.name}"...`, GAME_CONFIG.LOG_TYPE_EFFECT); const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL) ? this.gameState.opponent : this.gameState.player; const targetBaseStatsForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL) ? gameData.opponentBaseStats : gameData.playerBaseStats; serverGameLogic.applyAbilityEffect( ability, this.gameState.opponent, targetForAbility, gameData.opponentBaseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG ); actionProcessed = true; } } if (!actionProcessed) { // If AI chose an ability but it was invalid (e.g. due to error in AI logic) this.addToLog(`${this.gameState.opponent.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO); this.switchTurn(); // No delay, just switch return; } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // AI action processed, now switch turn (serverGameLogic.switchTurn handles its own delays/timing) this.switchTurn(); } checkGameOver() { // ... (код без изменений) if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true; const playerDead = this.gameState.player.currentHp <= 0; const opponentDead = this.gameState.opponent.currentHp <= 0; if (playerDead || opponentDead) { this.gameState.isGameOver = true; const winnerId = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const winner = this.gameState[winnerId]; const loserId = winnerId === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const loser = this.gameState[loserId]; const winnerName = winnerId === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats.name : gameData.opponentBaseStats.name; const loserName = loserId === GAME_CONFIG.PLAYER_ID ? gameData.playerBaseStats.name : gameData.opponentBaseStats.name; this.addToLog(`ПОБЕДА! ${winnerName} одолел(а) ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM); if (winnerId === GAME_CONFIG.PLAYER_ID && this.mode === 'ai') { const opponentNearDefeatTaunt = serverGameLogic.getElenaTaunt( 'opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState ); if (opponentNearDefeatTaunt && opponentNearDefeatTaunt !== "(Молчание)") { this.addToLog(`${this.gameState.player.name}: "${opponentNearDefeatTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO); } this.addToLog(`Елена исполнила свой тяжкий долг. ${gameData.opponentBaseStats.name} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM); } this.io.to(this.id).emit('gameOver', { winnerId: winnerId, reason: playerDead ? `${loserName} побежден(а)` : `${winnerName} побежден(а)`, // Corrected: Loser is playerDead means player lost finalGameState: this.gameState, log: this.consumeLogBuffer() }); return true; } return false; } addToLog(message, type) { 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() { if (this.logBuffer.length > 0) { this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() }); } } } module.exports = GameInstance;