// /server/game/instance/GameInstance.js const { v4: uuidv4 } = require('uuid'); const TurnTimer = require('./TurnTimer'); const gameLogic = require('../logic'); // Импортирует index.js из папки logic const dataUtils = require('../../data/dataUtils'); const GAME_CONFIG = require('../../core/config'); // <--- УБЕДИТЕСЬ, ЧТО GAME_CONFIG ИМПОРТИРОВАН class GameInstance { constructor(gameId, io, mode = 'ai', gameManager) { this.id = gameId; this.io = io; this.mode = mode; this.players = {}; this.playerSockets = {}; this.playerCount = 0; this.gameState = null; this.aiOpponent = (mode === 'ai'); this.logBuffer = []; this.playerCharacterKey = null; this.opponentCharacterKey = null; this.ownerIdentifier = null; this.gameManager = gameManager; this.turnTimer = new TurnTimer( GAME_CONFIG.TURN_DURATION_MS, GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS, () => this.handleTurnTimeout(), (remainingTime, isPlayerTurnForTimer) => { this.io.to(this.id).emit('turnTimerUpdate', { remainingTime, isPlayerTurn: isPlayerTurnForTimer }); } ); if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') { console.error(`[GameInstance ${this.id}] CRITICAL ERROR: GameManager reference invalid.`); } console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}.`); } addPlayer(socket, chosenCharacterKey = 'elena', identifier) { if (this.players[socket.id]) { socket.emit('gameError', { message: 'Ваш сокет уже зарегистрирован в этой игре.' }); return false; } const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier); if (existingPlayerByIdentifier) { socket.emit('gameError', { message: 'Вы уже находитесь в этой игре под другим подключением.' }); return false; } if (this.playerCount >= 2) { socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); return false; } let assignedPlayerId; let actualCharacterKey; if (this.mode === 'ai') { if (this.playerCount > 0) { socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); return false; } assignedPlayerId = GAME_CONFIG.PLAYER_ID; actualCharacterKey = 'elena'; this.ownerIdentifier = identifier; } else { // PvP if (this.playerCount === 0) { assignedPlayerId = GAME_CONFIG.PLAYER_ID; actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena'; this.ownerIdentifier = identifier; } else { assignedPlayerId = GAME_CONFIG.OPPONENT_ID; const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); actualCharacterKey = (firstPlayerInfo?.chosenCharacterKey === 'elena') ? 'almagest' : 'elena'; } } this.players[socket.id] = { id: assignedPlayerId, socket: socket, chosenCharacterKey: actualCharacterKey, identifier: identifier }; this.playerSockets[assignedPlayerId] = socket; this.playerCount++; socket.join(this.id); const characterBaseStats = dataUtils.getCharacterBaseStats(actualCharacterKey); console.log(`[GameInstance ${this.id}] Игрок ${identifier} (сокет ${socket.id}) (${characterBaseStats?.name || 'N/A'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Игроков: ${this.playerCount}.`); return true; } removePlayer(socketId) { const playerInfo = this.players[socketId]; if (playerInfo) { const playerRole = playerInfo.id; console.log(`[GameInstance ${this.id}] Игрок ${playerInfo.identifier} (сокет: ${socketId}, роль: ${playerRole}) покинул игру.`); if (playerInfo.socket) { try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ } } delete this.players[socketId]; this.playerCount--; if (this.playerSockets[playerRole]?.id === socketId) { delete this.playerSockets[playerRole]; } if (this.gameState && !this.gameState.isGameOver) { const isTurnOfDisconnected = (this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.PLAYER_ID) || (!this.gameState.isPlayerTurn && playerRole === GAME_CONFIG.OPPONENT_ID); if (isTurnOfDisconnected) this.turnTimer.clear(); } } } initializeGame() { console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Игроков: ${this.playerCount}.`); if (this.mode === 'ai' && this.playerCount === 1) { this.playerCharacterKey = 'elena'; this.opponentCharacterKey = 'balard'; } else if (this.mode === 'pvp' && this.playerCount === 2) { const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena'; this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; } else if (this.mode === 'pvp' && this.playerCount === 1) { const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena'; this.opponentCharacterKey = null; } else { console.error(`[GameInstance ${this.id}] Некорректное состояние для инициализации!`); return false; } const playerData = dataUtils.getCharacterData(this.playerCharacterKey); let opponentData = null; const isOpponentDefined = !!this.opponentCharacterKey; if (isOpponentDefined) opponentData = dataUtils.getCharacterData(this.opponentCharacterKey); if (!playerData || (isOpponentDefined && !opponentData)) { this._handleCriticalError('init_char_data_fail', 'Ошибка загрузки данных персонажей при инициализации.'); return false; } if (isOpponentDefined && (!opponentData.baseStats.maxHp || opponentData.baseStats.maxHp <= 0)) { this._handleCriticalError('init_opponent_hp_fail', 'Некорректные HP оппонента при инициализации.'); return false; } this.gameState = { player: this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities), opponent: isOpponentDefined ? this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) : this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание игрока...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []), // Плейсхолдер isPlayerTurn: isOpponentDefined ? Math.random() < 0.5 : true, isGameOver: false, turnNumber: 1, gameMode: this.mode }; if (isOpponentDefined) { this.logBuffer = []; this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM); const pCharKey = this.gameState.player.characterKey; const oCharKey = this.gameState.opponent.characterKey; // Нужен ключ оппонента для контекста if ((pCharKey === 'elena' || pCharKey === 'almagest') && oCharKey) { const opponentFullDataForTaunt = dataUtils.getCharacterData(oCharKey); // Получаем полные данные оппонента const startTaunt = gameLogic.getRandomTaunt(pCharKey, 'battleStart', {}, GAME_CONFIG, opponentFullDataForTaunt, this.gameState); if (startTaunt !== "(Молчание)") this.addToLog(`${this.gameState.player.name}: "${startTaunt}"`, GAME_CONFIG.LOG_TYPE_INFO); } } console.log(`[GameInstance ${this.id}] Состояние игры инициализировано. Готовность к старту: ${isOpponentDefined}`); return isOpponentDefined; } _createFighterState(roleId, baseStats, abilities) { const fighterState = { id: roleId, characterKey: baseStats.characterKey, name: baseStats.name, 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 => { // Добавлена проверка abilities 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() { if (!this.gameState || !this.gameState.opponent?.characterKey) { this._handleCriticalError('start_game_not_ready', 'Попытка старта не полностью готовой игры.'); return; } console.log(`[GameInstance ${this.id}] Запуск игры.`); const pData = dataUtils.getCharacterData(this.playerCharacterKey); const oData = dataUtils.getCharacterData(this.opponentCharacterKey); if (!pData || !oData) { this._handleCriticalError('start_char_data_fail', 'Ошибка данных персонажей при старте.'); return; } Object.values(this.players).forEach(playerInfo => { if (playerInfo.socket?.connected) { const dataForClient = 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, ...dataForClient, log: this.consumeLogBuffer(), 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(); this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); if (!this.gameState.isPlayerTurn && this.aiOpponent) { setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } } processPlayerAction(requestingSocketId, actionData) { if (!this.gameState || this.gameState.isGameOver) return; const actingPlayerInfo = this.players[requestingSocketId]; if (!actingPlayerInfo) { console.error(`[GameInstance ${this.id}] Действие от неизвестного сокета ${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) { console.warn(`[GameInstance ${this.id}] Действие от ${actingPlayerInfo.identifier}: не его ход.`); return; } 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]; const attackerData = dataUtils.getCharacterData(attackerState.characterKey); const defenderData = dataUtils.getCharacterData(defenderState.characterKey); if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail', 'Ошибка данных персонажа при действии.'); return; } let actionValid = true; let tauntContextTargetData = defenderData; // Данные цели для контекста насмешек if (actionData.actionType === 'attack') { const taunt = gameLogic.getRandomTaunt(attackerState.characterKey, 'basicAttack', {}, GAME_CONFIG, tauntContextTargetData, this.gameState); if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData); const delayedBuff = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK)); if (delayedBuff && !delayedBuff.justCast) { const regen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerData.baseStats.maxResource - attackerState.currentResource); if (regen > 0) { attackerState.currentResource = Math.round(attackerState.currentResource + regen); this.addToLog(`🌿 ${attackerState.name} восстанавливает ${regen} ${attackerState.resourceName} от "${delayedBuff.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL); } } } else if (actionData.actionType === 'ability' && actionData.abilityId) { const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId); if (!ability) { actionValid = false; actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." }); this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); // Перезапуск таймера return; } const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG); if (!validityCheck.isValid) { this.addToLog(validityCheck.reason, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (actionValid) { attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost); const taunt = gameLogic.getRandomTaunt(attackerState.characterKey, 'selfCastAbility', { abilityId: ability.id }, GAME_CONFIG, tauntContextTargetData, this.gameState); if (taunt !== "(Молчание)") this.addToLog(`${attackerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData); gameLogic.setAbilityCooldown(ability, attackerState, GAME_CONFIG); } } else { console.warn(`[GameInstance ${this.id}] Неизвестный тип действия: ${actionData?.actionType}`); actionValid = false; } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } if (actionValid) { setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } else { this.broadcastLogUpdate(); this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); // Перезапуск таймера } } switchTurn() { if (!this.gameState || this.gameState.isGameOver) return; this.turnTimer.clear(); const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const endingTurnActor = this.gameState[endingTurnActorRole]; const endingTurnData = dataUtils.getCharacterData(endingTurnActor.characterKey); if (!endingTurnData) { this._handleCriticalError('switch_turn_data_fail', 'Ошибка данных при смене хода.'); return; } gameLogic.processEffects(endingTurnActor.activeEffects, endingTurnActor, endingTurnData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils); gameLogic.updateBlockingStatus(this.gameState.player); gameLogic.updateBlockingStatus(this.gameState.opponent); if (endingTurnActor.abilityCooldowns && endingTurnData.abilities) gameLogic.processPlayerAbilityCooldowns(endingTurnActor.abilityCooldowns, endingTurnData.abilities, endingTurnActor.name, this.addToLog.bind(this), GAME_CONFIG); if (endingTurnActor.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActor); if (endingTurnActor.disabledAbilities?.length > 0 && endingTurnData.abilities) gameLogic.processDisabledAbilities(endingTurnActor.disabledAbilities, endingTurnData.abilities, endingTurnActor.name, this.addToLog.bind(this), GAME_CONFIG); if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; if (this.gameState.isPlayerTurn) this.gameState.turnNumber++; const currentTurnActor = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent; this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActor.name} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); if (!this.gameState.isPlayerTurn && this.aiOpponent) { setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } } processAiTurn() { if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.gameState.opponent?.characterKey !== 'balard') { if (this.gameState && !this.gameState.isGameOver) this.switchTurn(); return; } const attacker = this.gameState.opponent; const defender = this.gameState.player; const attackerData = dataUtils.getCharacterData('balard'); const defenderData = dataUtils.getCharacterData(defender.characterKey); if (!attackerData || !defenderData) { this._handleCriticalError('ai_char_data_fail', 'Ошибка данных AI.'); this.switchTurn(); return; } if (gameLogic.isCharacterFullySilenced(attacker, GAME_CONFIG)) { this.addToLog(`😵 ${attacker.name} под действием Безмолвия! Атакует в смятении.`, GAME_CONFIG.LOG_TYPE_EFFECT); gameLogic.performAttack(attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, defenderData); if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); return; } const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this)); let tauntContextTargetData = defenderData; if (aiDecision.actionType === 'attack') { gameLogic.performAttack(attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData); } else if (aiDecision.actionType === 'ability' && aiDecision.ability) { attacker.currentResource = Math.round(attacker.currentResource - aiDecision.ability.cost); gameLogic.applyAbilityEffect(aiDecision.ability, attacker, defender, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, tauntContextTargetData); gameLogic.setAbilityCooldown(aiDecision.ability, attacker, GAME_CONFIG); } // 'pass' уже залогирован в decideAiAction if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } checkGameOver() { if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true; if (!this.gameState.player || !this.gameState.opponent?.characterKey) return false; const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode); if (gameOverResult.isOver) { this.gameState.isGameOver = true; this.turnTimer.clear(); this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); const winnerState = this.gameState[gameOverResult.winnerRole]; const loserState = this.gameState[gameOverResult.loserRole]; if (winnerState && (winnerState.characterKey === 'elena' || winnerState.characterKey === 'almagest') && loserState) { const loserFullData = dataUtils.getCharacterData(loserState.characterKey); if (loserFullData) { // Убедимся, что данные проигравшего есть const taunt = gameLogic.getRandomTaunt(winnerState.characterKey, 'opponentNearDefeatCheck', {}, GAME_CONFIG, loserFullData, this.gameState); if (taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); } } if (loserState) { if (loserState.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserState.name} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM); else if (loserState.characterKey === 'almagest') this.addToLog(`Над полем битвы воцаряется тишина. ${loserState.name} побежден(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM); else if (loserState.characterKey === 'elena') this.addToLog(`Свет погас. ${loserState.name} повержен(а).`, GAME_CONFIG.LOG_TYPE_SYSTEM); } 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, gameOverResult.reason); return true; } return false; } endGameDueToDisconnect(disconnectedSocketId, disconnectedPlayerRole, disconnectedCharacterKey) { if (this.gameState && !this.gameState.isGameOver) { this.gameState.isGameOver = true; this.turnTimer.clear(); const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'opponent_disconnected', disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID, // winner disconnectedPlayerRole // loser ); this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); console.log(`[GameInstance ${this.id}] Игра завершена из-за дисконнекта. Победитель: ${result.winnerRole || 'Нет'}. Отключился: ${disconnectedPlayerRole}.`); this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: disconnectedCharacterKey // Ключ того, кто отключился }); this.gameManager._cleanupGame(this.id, result.reason); } } handleTurnTimeout() { if (!this.gameState || this.gameState.isGameOver) return; // this.turnTimer.clear(); // TurnTimer сам себя очистит при вызове onTimeout const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerPlayerRole, timedOutPlayerRole); if (!this.gameState[winnerPlayerRole]?.characterKey) { // Если победитель не определен (например, ожидание в PvP) this._handleCriticalError('timeout_winner_undefined', `Таймаут, но победитель (${winnerPlayerRole}) не определен.`); return; } this.gameState.isGameOver = true; // Устанавливаем здесь, т.к. getGameOverResult мог не знать, что игра уже окончена this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); console.log(`[GameInstance ${this.id}] Таймаут хода для ${this.gameState[timedOutPlayerRole]?.name}. Победитель: ${this.gameState[winnerPlayerRole]?.name}.`); 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, result.reason); } _handleCriticalError(reasonCode, logMessage) { console.error(`[GameInstance ${this.id}] КРИТИЧЕСКАЯ ОШИБКА: ${logMessage} (Код: ${reasonCode})`); if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true; this.turnTimer.clear(); 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' }); if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') { this.gameManager._cleanupGame(this.id, `critical_error_${reasonCode}`); } } 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() { if (this.logBuffer.length > 0) { this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() }); } } } module.exports = GameInstance;