// /server/game/instance/GameInstance.js const { v4: uuidv4 } = require('uuid'); const TurnTimer = require('./TurnTimer'); const gameLogic = require('../logic'); const dataUtils = require('../../data/dataUtils'); const GAME_CONFIG = require('../../core/config'); const PlayerConnectionHandler = require('./PlayerConnectionHandler'); class GameInstance { constructor(gameId, io, mode = 'ai', gameManager) { this.id = gameId; this.io = io; this.mode = mode; this.gameManager = gameManager; this.playerConnectionHandler = new PlayerConnectionHandler(this); this.gameState = null; this.aiOpponent = (mode === 'ai'); this.logBuffer = []; this.playerCharacterKey = null; this.opponentCharacterKey = null; this.ownerIdentifier = null; this.turnTimer = new TurnTimer( GAME_CONFIG.TURN_DURATION_MS, GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS, () => this.handleTurnTimeout(), (remainingTime, isPlayerTurnForTimer, isPaused) => { // Логируем отправку обновления таймера // console.log(`[GI TURN_TIMER_CB ${this.id}] Sending update. Remaining: ${remainingTime}, isPlayerT: ${isPlayerTurnForTimer}, isPaused (raw): ${isPaused}, effectivelyPaused: ${this.isGameEffectivelyPaused()}`); this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: isPlayerTurnForTimer, isPaused: isPaused || this.isGameEffectivelyPaused() }); }, this.id ); if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') { console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: Ссылка на GameManager недействительна.`); } console.log(`[GameInstance ${this.id}] Создан. Режим: ${mode}. PlayerConnectionHandler также инициализирован.`); } get playerCount() { return this.playerConnectionHandler.playerCount; } get players() { return this.playerConnectionHandler.getAllPlayersInfo(); } setPlayerCharacterKey(key) { this.playerCharacterKey = key; } setOpponentCharacterKey(key) { this.opponentCharacterKey = key; } setOwnerIdentifier(identifier) { this.ownerIdentifier = identifier; } addPlayer(socket, chosenCharacterKey, identifier) { return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier); } removePlayer(socketId, reason) { this.playerConnectionHandler.removePlayer(socketId, reason); } handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) { this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId); } handlePlayerReconnected(playerIdRole, newSocket) { console.log(`[GameInstance ${this.id}] Делегирование handlePlayerReconnected в PCH для роли ${playerIdRole}, сокет ${newSocket.id}`); return this.playerConnectionHandler.handlePlayerReconnected(playerIdRole, newSocket); } clearAllReconnectTimers() { this.playerConnectionHandler.clearAllReconnectTimers(); } isGameEffectivelyPaused() { return this.playerConnectionHandler.isGameEffectivelyPaused(); } handlePlayerPermanentlyLeft(playerRole, characterKey, reason) { console.log(`[GameInstance ${this.id}] Игрок окончательно покинул игру. Роль: ${playerRole}, Персонаж: ${characterKey}, Причина: ${reason}`); if (this.gameState && !this.gameState.isGameOver) { if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) { this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game"); } else if (this.mode === 'pvp') { if (this.playerCount < 2) { const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected); this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id); } } } else if (!this.gameState && Object.keys(this.players).length === 0) { this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi_via_pch"); } } _sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) { if (!characterState || !characterState.characterKey) return; if (!opponentCharacterKey) return; if (!gameLogic.getRandomTaunt) { console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt недоступен!`); return; } if (!this.gameState) return; let context = {}; let subTrigger = null; if (typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number') { subTrigger = subTriggerOrContext; } else if (typeof subTriggerOrContext === 'object' && subTriggerOrContext !== null) { context = { ...subTriggerOrContext }; } context = { ...context, ...contextOverrides }; if ((triggerType === 'selfCastAbility' || triggerType === 'onOpponentAction') && (typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number')) { context.abilityId = subTriggerOrContext; subTrigger = subTriggerOrContext; } else if (triggerType === 'onBattleState' && typeof subTriggerOrContext === 'string') { subTrigger = subTriggerOrContext; } else if (triggerType === 'basicAttack' && typeof subTriggerOrContext === 'string') { subTrigger = subTriggerOrContext; } const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey); if (!opponentFullData) return; const tauntText = gameLogic.getRandomTaunt( characterState.characterKey, triggerType, subTrigger || context, GAME_CONFIG, opponentFullData, this.gameState ); if (tauntText && tauntText !== "(Молчание)") { this.addToLog(`${characterState.name}: "${tauntText}"`, GAME_CONFIG.LOG_TYPE_INFO); } } initializeGame() { console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Активных игроков (PCH): ${this.playerCount}. Всего записей в PCH.players: ${Object.keys(this.players).length}. PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`); const p1ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); const p2ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected); // Устанавливаем ключи персонажей, если они еще не установлены, на основе активных игроков в PCH // Это важно, если initializeGame вызывается до того, как PCH успел обновить ключи в GI через сеттеры if (p1ActiveEntry && !this.playerCharacterKey) this.playerCharacterKey = p1ActiveEntry.chosenCharacterKey; if (p2ActiveEntry && !this.opponentCharacterKey && this.mode === 'pvp') this.opponentCharacterKey = p2ActiveEntry.chosenCharacterKey; if (this.mode === 'ai') { if (!p1ActiveEntry) { this._handleCriticalError('init_ai_no_active_player_gi', 'Инициализация AI игры: Игрок-человек не найден или не активен.'); return false; } if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'Инициализация AI игры: Ключ персонажа игрока не установлен.'); return false;} this.opponentCharacterKey = 'balard'; } else { // pvp if (this.playerCount === 1 && p1ActiveEntry && !this.playerCharacterKey) { this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP инициализация (1 игрок): Ключ персонажа игрока отсутствует.'); return false; } if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) { this._handleCriticalError('init_pvp_char_key_missing_gi', `Инициализация PvP: playerCount=2, но ключ персонажа отсутствует. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`); return false; } } const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null; const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null; const isPlayerSlotFilledAndActive = !!(playerData && p1ActiveEntry); const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2ActiveEntry)); if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !opponentData) ) { this._handleCriticalError('init_ai_data_fail_gs_gi', 'Инициализация AI игры: Не удалось загрузить данные игрока или AI для gameState.'); return false; } this.logBuffer = []; // Имена берутся из playerData/opponentData, если они есть. PCH обновит их при реконнекте, если они изменились. const playerName = playerData?.baseStats?.name || (p1ActiveEntry?.name || 'Ожидание Игрока 1...'); let opponentName; if (this.mode === 'ai') { opponentName = opponentData?.baseStats?.name || 'Противник AI'; } else { opponentName = opponentData?.baseStats?.name || (p2ActiveEntry?.name || 'Ожидание Игрока 2...'); } this.gameState = { player: isPlayerSlotFilledAndActive ? this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities, playerName) : // Передаем имя this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: playerName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], playerName), opponent: isOpponentSlotFilledAndActive ? this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities, opponentName) : // Передаем имя this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: opponentName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], opponentName), isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true, isGameOver: false, turnNumber: 1, gameMode: this.mode }; console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Игрок: ${this.gameState.player.name} (${this.gameState.player.characterKey}). Оппонент: ${this.gameState.opponent.name} (${this.gameState.opponent.characterKey}). IsPlayerTurn: ${this.gameState.isPlayerTurn}. Готово к старту: AI=${isPlayerSlotFilledAndActive && !!opponentData}, PvP1=${isPlayerSlotFilledAndActive}, PvP2=${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`); return (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && !!opponentData) : isPlayerSlotFilledAndActive; } _createFighterState(roleId, baseStats, abilities, explicitName = null) { const fighterState = { id: roleId, characterKey: baseStats.characterKey, name: explicitName || baseStats.name, // Используем explicitName если передано currentHp: baseStats.maxHp, maxHp: baseStats.maxHp, currentResource: baseStats.maxResource, maxResource: baseStats.maxResource, resourceName: baseStats.resourceName, attackPower: baseStats.attackPower, isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {} }; (abilities || []).forEach(ability => { if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) { fighterState.abilityCooldowns[ability.id] = 0; } }); if (baseStats.characterKey === 'balard') { fighterState.silenceCooldownTurns = 0; fighterState.manaDrainCooldownTurns = 0; } return fighterState; } startGame() { console.log(`[GameInstance ${this.id}] Попытка запуска игры. Paused: ${this.isGameEffectivelyPaused()}`); if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Запуск игры отложен: игра на паузе.`); return; } if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) { console.warn(`[GameInstance ${this.id}] startGame: gameState или ключи персонажей не полностью инициализированы. Попытка повторной инициализации.`); if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) { this._handleCriticalError('start_game_reinit_failed_sg_gi', 'Повторная инициализация перед стартом не удалась или ключи все еще отсутствуют в gameState.'); return; } } console.log(`[GameInstance ${this.id}] Запуск игры. Игрок в GS: ${this.gameState.player.name} (${this.playerCharacterKey}), Оппонент в GS: ${this.gameState.opponent.name} (${this.opponentCharacterKey}). IsPlayerTurn: ${this.gameState.isPlayerTurn}`); const pData = dataUtils.getCharacterData(this.playerCharacterKey); const oData = dataUtils.getCharacterData(this.opponentCharacterKey); if (!pData || !oData) { this._handleCriticalError('start_char_data_fail_sg_gi', `Не удалось загрузить данные персонажей при старте игры. PData: ${!!pData}, OData: ${!!oData}`); return; } // Обновляем имена в gameState на основе данных персонажей перед отправкой клиентам // Это гарантирует, что имена из dataUtils (самые "правильные") попадут в первое gameStarted if (this.gameState.player && pData?.baseStats?.name) this.gameState.player.name = pData.baseStats.name; if (this.gameState.opponent && oData?.baseStats?.name) this.gameState.opponent.name = oData.baseStats.name; this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM); if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) { this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start'); this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start'); } else { console.warn(`[GameInstance ${this.id}] Не удалось произнести стартовые насмешки во время startGame, gameState акторы/ключи не полностью готовы.`); } const initialLog = this.consumeLogBuffer(); Object.values(this.players).forEach(playerInfo => { if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) { const dataForThisClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ? { playerBaseStats: pData.baseStats, opponentBaseStats: oData.baseStats, playerAbilities: pData.abilities, opponentAbilities: oData.abilities } : { playerBaseStats: oData.baseStats, opponentBaseStats: pData.baseStats, playerAbilities: oData.abilities, opponentAbilities: pData.abilities }; playerInfo.socket.emit('gameStarted', { gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState, ...dataForThisClient, log: [...initialLog], clientConfig: { ...GAME_CONFIG } }); } }); const firstTurnActor = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent; this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${firstTurnActor.name} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastLogUpdate(); const isFirstTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn; console.log(`[GameInstance ${this.id}] Запуск таймера в startGame. isPlayerTurn: ${this.gameState.isPlayerTurn}, isFirstTurnAi: ${isFirstTurnAi}`); this.turnTimer.start(this.gameState.isPlayerTurn, isFirstTurnAi); if (isFirstTurnAi) { setTimeout(() => { if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver && this.mode === 'ai' && !this.gameState.isPlayerTurn) { this.processAiTurn(); } }, GAME_CONFIG.DELAY_OPPONENT_TURN); } } processPlayerAction(identifier, actionData) { console.log(`[GameInstance ${this.id}] processPlayerAction от ${identifier}. Действие: ${actionData.actionType}. Текущий GS.isPlayerTurn: ${this.gameState?.isPlayerTurn}. Paused: ${this.isGameEffectivelyPaused()}`); const actingPlayerInfo = Object.values(this.players).find(p => p.identifier === identifier); if (!actingPlayerInfo || !actingPlayerInfo.socket) { console.error(`[GameInstance ${this.id}] Действие от неизвестного или безсокетного идентификатора ${identifier}.`); return; } if (this.isGameEffectivelyPaused()) { actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."}); return; } if (!this.gameState || this.gameState.isGameOver) { 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) { console.warn(`[GameInstance ${this.id}] Неверный ход! Игрок ${identifier} (роль ${actingPlayerRole}) пытался действовать. GS.isPlayerTurn: ${this.gameState.isPlayerTurn}`); actingPlayerInfo.socket.emit('gameError', { message: "Не ваш ход." }); return; } console.log(`[GameInstance ${this.id}] Ход корректен. Очистка таймера.`); if(this.turnTimer.isActive()) this.turnTimer.clear(); 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]; if (!attackerState || !attackerState.characterKey || !defenderState || !defenderState.characterKey) { this._handleCriticalError('action_actor_state_invalid_gi', `Состояние/ключ Атакующего или Защитника недействительны.`); return; } const attackerData = dataUtils.getCharacterData(attackerState.characterKey); const defenderData = dataUtils.getCharacterData(defenderState.characterKey); if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi', 'Ошибка данных персонажа при действии.'); return; } let actionIsValidAndPerformed = false; if (actionData.actionType === 'attack') { this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack'); gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt); actionIsValidAndPerformed = true; } else if (actionData.actionType === 'ability' && actionData.abilityId) { const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId); if (!ability) { actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." }); } else { const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG); if (validityCheck.isValid) { this._sayTaunt(attackerState, defenderState.characterKey, 'selfCastAbility', ability.id); attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost); gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null); gameLogic.setAbilityCooldown(ability, attackerState, GAME_CONFIG); actionIsValidAndPerformed = true; } else { this.addToLog(validityCheck.reason || `${attackerState.name} не может использовать "${ability.name}".`, GAME_CONFIG.LOG_TYPE_INFO); actionIsValidAndPerformed = false; } } } else { actionIsValidAndPerformed = false; } if (this.checkGameOver()) return; this.broadcastLogUpdate(); if (actionIsValidAndPerformed) { setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } else { const isAiTurnForTimer = this.mode === 'ai' && !this.gameState.isPlayerTurn; console.log(`[GameInstance ${this.id}] Действие не выполнено, перезапуск таймера. isPlayerTurn: ${this.gameState.isPlayerTurn}, isAiTurnForTimer: ${isAiTurnForTimer}`); this.turnTimer.start(this.gameState.isPlayerTurn, isAiTurnForTimer); } } switchTurn() { console.log(`[GameInstance ${this.id}] Попытка смены хода. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}`); if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Смена хода отложена: игра на паузе.`); return; } if (!this.gameState || this.gameState.isGameOver) { return; } if(this.turnTimer.isActive()) this.turnTimer.clear(); const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const endingTurnActorState = this.gameState[endingTurnActorRole]; if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid_gi', `Состояние или ключ актора, завершающего ход, недействительны.`); return; } const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey); if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Отсутствуют данные персонажа.`); return; } gameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils); gameLogic.updateBlockingStatus(endingTurnActorState); if (endingTurnActorState.abilityCooldowns && endingTurnActorData.abilities) gameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnActorData.abilities, endingTurnActorState.name, this.addToLog.bind(this), GAME_CONFIG); if (endingTurnActorState.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActorState); if (endingTurnActorState.disabledAbilities?.length > 0 && endingTurnActorData.abilities) gameLogic.processDisabledAbilities(endingTurnActorState.disabledAbilities, endingTurnActorData.abilities, endingTurnActorState.name, this.addToLog.bind(this), GAME_CONFIG); if (this.checkGameOver()) return; this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; if (this.gameState.isPlayerTurn) this.gameState.turnNumber++; const currentTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const currentTurnActorState = this.gameState[currentTurnActorRole]; if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Состояние или имя текущего актора недействительны.`); return; } this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole); if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) { console.log(`[GameInstance ${this.id}] Ход перешел к ${currentTurnActorRole}, но игрок ${currentTurnPlayerEntry.identifier} отключен. Таймер не запущен switchTurn.`); } else { const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn; console.log(`[GameInstance ${this.id}] Запуск таймера в switchTurn. isPlayerTurn: ${this.gameState.isPlayerTurn}, isNextTurnAi: ${isNextTurnAi}`); this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi); if (isNextTurnAi) { setTimeout(() => { if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver && this.mode === 'ai' && !this.gameState.isPlayerTurn) { this.processAiTurn(); } }, GAME_CONFIG.DELAY_OPPONENT_TURN); } } } processAiTurn() { console.log(`[GameInstance ${this.id}] processAiTurn. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}, IsPlayerTurn: ${this.gameState?.isPlayerTurn}`); if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Ход AI отложен: игра на паузе.`); return; } if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; } if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) { console.error(`[GameInstance ${this.id}] AI не Балард! Персонаж AI: ${this.gameState.opponent?.characterKey}. Принудительная смена хода.`); this.switchTurn(); return; } if(this.turnTimer.isActive()) this.turnTimer.clear(); const aiState = this.gameState.opponent; const playerState = this.gameState.player; if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid_gi', 'Состояние игрока недействительно для хода AI.'); return; } const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this)); let actionIsValidAndPerformedForAI = false; if (aiDecision.actionType === 'attack') { this._sayTaunt(aiState, playerState.characterKey, 'basicAttack'); gameLogic.performAttack(aiState, playerState, dataUtils.getCharacterBaseStats(aiState.characterKey), dataUtils.getCharacterBaseStats(playerState.characterKey), this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt); actionIsValidAndPerformedForAI = true; } else if (aiDecision.actionType === 'ability' && aiDecision.ability) { this._sayTaunt(aiState, playerState.characterKey, 'selfCastAbility', aiDecision.ability.id); aiState.currentResource = Math.round(aiState.currentResource - aiDecision.ability.cost); gameLogic.applyAbilityEffect(aiDecision.ability, aiState, playerState, dataUtils.getCharacterBaseStats(aiState.characterKey), dataUtils.getCharacterBaseStats(playerState.characterKey), this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null); gameLogic.setAbilityCooldown(aiDecision.ability, aiState, GAME_CONFIG); actionIsValidAndPerformedForAI = true; } else if (aiDecision.actionType === 'pass') { if (aiDecision.logMessage && this.addToLog) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type); else if(this.addToLog) this.addToLog(`${aiState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO); actionIsValidAndPerformedForAI = true; } if (this.checkGameOver()) return; this.broadcastLogUpdate(); if (actionIsValidAndPerformedForAI) { setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } else { console.error(`[GameInstance ${this.id}] AI не смог выполнить действие. Принудительная смена хода.`); setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } } checkGameOver() { if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true; if (!this.gameState.isGameOver && this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) { const player = this.gameState.player; const opponent = this.gameState.opponent; const pData = dataUtils.getCharacterData(player.characterKey); const oData = dataUtils.getCharacterData(opponent.characterKey); if (pData && oData) { const nearDefeatThreshold = GAME_CONFIG.OPPONENT_NEAR_DEFEAT_THRESHOLD_PERCENT || 0.2; if (opponent.currentHp > 0 && (opponent.currentHp / oData.baseStats.maxHp) <= nearDefeatThreshold) { this._sayTaunt(player, opponent.characterKey, 'onBattleState', 'opponentNearDefeat'); } if (player.currentHp > 0 && (player.currentHp / pData.baseStats.maxHp) <= nearDefeatThreshold) { this._sayTaunt(opponent, player.characterKey, 'onBattleState', 'opponentNearDefeat'); } } } const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode); if (gameOverResult.isOver) { this.gameState.isGameOver = true; if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); const winnerState = this.gameState[gameOverResult.winnerRole]; const loserState = this.gameState[gameOverResult.loserRole]; if (winnerState?.characterKey && loserState?.characterKey) { this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat'); } console.log(`[GameInstance ${this.id}] Игра окончена. Победитель: ${gameOverResult.winnerRole || 'Нет'}. Причина: ${gameOverResult.reason}.`); this.io.to(this.id).emit('gameOver', { winnerId: gameOverResult.winnerRole, reason: gameOverResult.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: loserState?.characterKey || 'unknown' }); this.gameManager._cleanupGame(this.id, `game_ended_${gameOverResult.reason}`); return true; } return false; } endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) { if (this.gameState && !this.gameState.isGameOver) { this.gameState.isGameOver = true; if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); let actualWinnerRole = winnerIfAny; let winnerActuallyExists = false; if (actualWinnerRole) { const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected); if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) { winnerActuallyExists = !!this.gameState.opponent?.characterKey; } else if (winnerPlayerEntry) { winnerActuallyExists = true; } } if (!winnerActuallyExists) { actualWinnerRole = (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID); const defaultWinnerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected); if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) { winnerActuallyExists = !!this.gameState.opponent?.characterKey; } else if (defaultWinnerEntry) { winnerActuallyExists = true; } } const finalWinnerRole = winnerActuallyExists ? actualWinnerRole : null; const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, reason, finalWinnerRole, disconnectedPlayerRole); this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); console.log(`[GameInstance ${this.id}] Игра завершена из-за отключения: ${reason}. Победитель: ${result.winnerRole || 'Нет'}.`); this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: disconnectedCharacterKey, disconnectedCharacterName: (reason === 'opponent_disconnected' || reason === 'player_left_ai_game' || reason === 'opponent_left_pvp_game') ? (this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined }); this.gameManager._cleanupGame(this.id, `disconnect_game_ended_gi_${result.reason}`); } else if (this.gameState?.isGameOver) { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: игра уже была завершена.`); this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup_gi`); } else { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: нет gameState.`); this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup_gi`); } } playerExplicitlyLeftAiGame(identifier) { if (this.mode !== 'ai' || (this.gameState && this.gameState.isGameOver)) { console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame вызван, но не режим AI или игра завершена.`); if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over_gi`); return; } const playerEntry = Object.values(this.players).find(p => p.identifier === identifier); if (!playerEntry || playerEntry.id !== GAME_CONFIG.PLAYER_ID) { console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Идентификатор ${identifier} не является игроком-человеком или не найден.`); return; } console.log(`[GameInstance ${this.id}] Игрок ${identifier} явно покинул AI игру.`); if (this.gameState) { this.gameState.isGameOver = true; this.addToLog(`Игрок покинул битву с ${this.gameState.opponent?.name || 'AI'}.`, GAME_CONFIG.LOG_TYPE_SYSTEM); } else { this.addToLog(`Игрок покинул AI игру до ее полного начала.`, GAME_CONFIG.LOG_TYPE_SYSTEM); } if (this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); this.io.to(this.id).emit('gameOver', { winnerId: GAME_CONFIG.OPPONENT_ID, reason: "player_left_ai_game", finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: playerEntry.chosenCharacterKey }); this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly_gi'); } playerDidSurrender(surrenderingPlayerIdentifier) { console.log(`[GameInstance ${this.id}] playerDidSurrender вызван для идентификатора: ${surrenderingPlayerIdentifier}`); if (!this.gameState || this.gameState.isGameOver) { if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished_gi`); } console.warn(`[GameInstance ${this.id}] Попытка сдачи в неактивной/завершенной игре от ${surrenderingPlayerIdentifier}.`); return; } const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier); if (!surrenderedPlayerEntry) { console.error(`[GameInstance ${this.id}] Сдающийся игрок ${surrenderingPlayerIdentifier} не найден.`); return; } const surrenderingPlayerRole = surrenderedPlayerEntry.id; if (this.mode === 'ai') { if (surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID) { console.log(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} "сдался" (покинул) AI игру.`); this.playerExplicitlyLeftAiGame(surrenderingPlayerIdentifier); } else { console.warn(`[GameInstance ${this.id}] Сдача в AI режиме от не-игрока (роль: ${surrenderingPlayerRole}). Игнорируется.`); } return; } if (this.mode !== 'pvp') { console.warn(`[GameInstance ${this.id}] Сдача вызвана в не-PvP, не-AI режиме: ${this.mode}. Игнорируется.`); return; } const surrenderedPlayerName = this.gameState[surrenderingPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey; const surrenderedPlayerCharKey = this.gameState[surrenderingPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey; const winnerRole = surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const winnerName = this.gameState[winnerRole]?.name || `Оппонент`; const winnerCharKey = this.gameState[winnerRole]?.characterKey; this.gameState.isGameOver = true; if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); this.addToLog(`🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`, GAME_CONFIG.LOG_TYPE_SYSTEM); console.log(`[GameInstance ${this.id}] Игрок ${surrenderedPlayerName} (Роль: ${surrenderingPlayerRole}) сдался. Победитель: ${winnerName} (Роль: ${winnerRole}).`); if (winnerCharKey && surrenderedPlayerCharKey && this.gameState[winnerRole]) { this._sayTaunt(this.gameState[winnerRole], surrenderedPlayerCharKey, 'onBattleState', 'opponentNearDefeat'); } this.io.to(this.id).emit('gameOver', { winnerId: winnerRole, reason: "player_surrendered", finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: surrenderedPlayerCharKey }); this.gameManager._cleanupGame(this.id, "player_surrendered_gi"); } handleTurnTimeout() { if (!this.gameState || this.gameState.isGameOver) return; console.log(`[GameInstance ${this.id}] Произошел таймаут хода.`); const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; let winnerActuallyExists = false; if (this.mode === 'ai' && winnerPlayerRoleIfHuman === GAME_CONFIG.OPPONENT_ID) { winnerActuallyExists = !!this.gameState.opponent?.characterKey; } else { const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRoleIfHuman && !p.isTemporarilyDisconnected); winnerActuallyExists = !!winnerEntry; } const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : null, timedOutPlayerRole); this.gameState.isGameOver = true; this.clearAllReconnectTimers(); this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) { this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat'); } console.log(`[GameInstance ${this.id}] Ход истек для ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Победитель: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`); this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown' }); this.gameManager._cleanupGame(this.id, `timeout_gi_${result.reason}`); } _handleCriticalError(reasonCode, logMessage) { console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`); if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true; else if (!this.gameState) { this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode }; } if(this.turnTimer.isActive()) this.turnTimer.clear(); this.clearAllReconnectTimers(); this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.io.to(this.id).emit('gameOver', { winnerId: null, reason: `server_error_${reasonCode}`, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: 'unknown' }); this.gameManager._cleanupGame(this.id, `critical_error_gi_${reasonCode}`); } addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() }); // Раскомментируйте для немедленной отправки логов, если нужно (но обычно лучше батчинг) // this.broadcastLogUpdate(); } consumeLogBuffer() { const logs = [...this.logBuffer]; this.logBuffer = []; return logs; } broadcastGameStateUpdate() { if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] broadcastGameStateUpdate отложено: игра на паузе.`); return; } if (!this.gameState) { console.warn(`[GameInstance ${this.id}] broadcastGameStateUpdate: gameState отсутствует.`); return; } console.log(`[GameInstance ${this.id}] Отправка gameStateUpdate. IsPlayerTurn: ${this.gameState.isPlayerTurn}`); this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() }); } broadcastLogUpdate() { if (this.isGameEffectivelyPaused() && this.logBuffer.some(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM)) { const systemLogs = this.logBuffer.filter(log => log.type === GAME_CONFIG.LOG_TYPE_SYSTEM); if (systemLogs.length > 0) { this.io.to(this.id).emit('logUpdate', { log: systemLogs }); } this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); // Оставляем несистемные return; } if (this.logBuffer.length > 0) { this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() }); } } } module.exports = GameInstance;