// /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'); 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.reconnectTimers = {}; this.turnTimer = new TurnTimer( GAME_CONFIG.TURN_DURATION_MS, GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS, () => this.handleTurnTimeout(), (remainingTime, isPlayerTurnForTimer) => { if (!this.isGameEffectivelyPaused()) { 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) { console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`); const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier); if (existingPlayerByIdentifier) { console.warn(`[GameInstance ${this.id}] Identifier ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`); if (existingPlayerByIdentifier.isTemporarilyDisconnected) { return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket); } // Если игра уже завершена, и игрок пытается "добавиться" (что маловероятно, если GameManager.handleRequestGameState работает корректно), // то addPlayer не должен успешно завершаться. if (this.gameState && this.gameState.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); return false; } 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 (!Object.values(this.players).some(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected)) { assignedPlayerId = GAME_CONFIG.PLAYER_ID; actualCharacterKey = (chosenCharacterKey === 'almagest' || chosenCharacterKey === 'balard') ? chosenCharacterKey : 'elena'; this.ownerIdentifier = identifier; } else if (!Object.values(this.players).some(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected)) { assignedPlayerId = GAME_CONFIG.OPPONENT_ID; const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); if (firstPlayerInfo?.chosenCharacterKey === 'elena') actualCharacterKey = 'almagest'; else if (firstPlayerInfo?.chosenCharacterKey === 'almagest') actualCharacterKey = 'elena'; else actualCharacterKey = 'balard'; // Default if first player is Balard or something else } else { socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' }); return false; } } const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId); if (oldPlayerSocketIdForRole) { console.log(`[GameInstance ${this.id}] Role ${assignedPlayerId} was previously occupied by socket ${oldPlayerSocketIdForRole}. Removing old entry.`); delete this.players[oldPlayerSocketIdForRole]; } this.players[socket.id] = { id: assignedPlayerId, socket: socket, chosenCharacterKey: actualCharacterKey, identifier: identifier, isTemporarilyDisconnected: false }; 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, reason = "unknown_reason_for_removal") { const playerInfo = this.players[socketId]; if (playerInfo) { const playerRole = playerInfo.id; console.log(`[GameInstance ${this.id}] Окончательное удаление игрока ${playerInfo.identifier} (сокет: ${socketId}, роль: ${playerRole}). Причина: ${reason}.`); if (playerInfo.socket) { try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ } } if (!playerInfo.isTemporarilyDisconnected) { // Только если он не был уже помечен как "временно отключен" this.playerCount--; } delete this.players[socketId]; if (this.playerSockets[playerRole]?.id === socketId) { delete this.playerSockets[playerRole]; } console.log(`[GameInstance ${this.id}] Игрок ${playerInfo.identifier} удален. Активных игроков: ${this.playerCount}.`); // Завершаем игру, если она была активна и стала неиграбельной 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(); if (this.mode === 'ai' && this.playerCount === 0) { console.log(`[GameInstance ${this.id}] AI игра стала пустой после удаления игрока. Завершение игры.`); this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "player_left_empty_ai_game"); } else if (this.mode === 'pvp' && this.playerCount < 2) { // Убедимся, что остался хотя бы один игрок, чтобы ему присудить победу. // Если playerCount стал 0, то победителя нет. const remainingPlayer = Object.values(this.players).find(p => !p.isTemporarilyDisconnected); const winnerRoleIfAny = remainingPlayer ? remainingPlayer.id : null; console.log(`[GameInstance ${this.id}] PvP игра стала неполной (${this.playerCount} игроков) после удаления игрока ${playerInfo.identifier} (роль ${playerRole}).`); this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "opponent_left_pvp_game", winnerRoleIfAny); } } } else { console.log(`[GameInstance ${this.id}] Попытка удалить игрока по socketId ${socketId}, но он не найден.`); } } handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft CALLED for role ${playerIdRole}, identifier ${identifier}, charKey ${characterKey}`); const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); if (!playerEntry || !playerEntry.socket) { console.warn(`[GameInstance ${this.id}] Не найден активный игрок ${identifier} (роль: ${playerIdRole}) для пометки как отключенного.`); return; } if (this.gameState && this.gameState.isGameOver) { console.log(`[GameInstance ${this.id}] Игра уже завершена, игнорируем 'potentiallyLeft' для ${identifier}.`); return; } if (playerEntry.isTemporarilyDisconnected) { console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль ${playerIdRole}) уже помечен как временно отключенный. Таймер должен быть активен.`); return; } playerEntry.isTemporarilyDisconnected = true; this.playerCount--; // Уменьшаем счетчик АКТИВНЫХ игроков console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) помечен как временно отключенный. Активных игроков: ${this.playerCount}. Запуск таймера реконнекта.`); const disconnectedPlayerName = this.gameState?.[playerIdRole]?.name || characterKey || `Игрок (роль ${playerIdRole})`; const disconnectLogMessage = `🔌 Игрок ${disconnectedPlayerName} отключился. Ожидание переподключения...`; this.addToLog(disconnectLogMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); this.io.to(this.id).emit('opponentDisconnected', { disconnectedPlayerId: playerIdRole, disconnectedCharacterName: disconnectedPlayerName, }); this.broadcastLogUpdate(); // Отправляем лог об отключении const currentTurnPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; if (currentTurnPlayerRole === playerIdRole) { this.turnTimer.clear(); console.log(`[GameInstance ${this.id}] Ход был за отключившимся игроком ${playerIdRole}, таймер хода остановлен.`); } this.clearReconnectTimer(playerIdRole); this.reconnectTimers[playerIdRole] = setTimeout(() => { console.log(`[GameInstance ${this.id}] Таймер реконнекта для игрока ${identifier} (роль: ${playerIdRole}) истек.`); delete this.reconnectTimers[playerIdRole]; const stillDisconnectedPlayerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier && p.isTemporarilyDisconnected); if (stillDisconnectedPlayerEntry) { console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) не переподключился. Удаляем окончательно.`); this.removePlayer(stillDisconnectedPlayerEntry.socket.id, "reconnect_timeout"); } else { console.log(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) уже переподключился или был удален ранее. Таймер истек без действия.`); } }, GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000); } handlePlayerReconnected(playerIdRole, newSocket) { const identifier = newSocket.userData?.userId; console.log(`[GameInstance ${this.id}] handlePlayerReconnected CALLED for role ${playerIdRole}, identifier ${identifier}, newSocket ${newSocket.id}`); if (this.gameState && this.gameState.isGameOver) { console.warn(`[GameInstance ${this.id}] Игрок ${identifier} (роль ${playerIdRole}) пытается переподключиться к уже ЗАВЕРШЕННОЙ игре. Отправка gameError.`); newSocket.emit('gameError', { message: 'Не удалось восстановить сессию: игра уже завершена.' }); // GameManager.handleRequestGameState должен был это перехватить, но на всякий случай. return false; } const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); if (playerEntry && playerEntry.isTemporarilyDisconnected) { this.clearReconnectTimer(playerIdRole); const oldSocketId = playerEntry.socket.id; if (this.players[oldSocketId]) { delete this.players[oldSocketId]; } playerEntry.socket = newSocket; playerEntry.isTemporarilyDisconnected = false; this.players[newSocket.id] = playerEntry; this.playerSockets[playerIdRole] = newSocket; this.playerCount++; // Восстанавливаем счетчик активных newSocket.join(this.id); const reconnectedPlayerName = this.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey || `Игрок (роль ${playerIdRole})`; console.log(`[GameInstance ${this.id}] Игрок ${identifier} (${reconnectedPlayerName}) успешно переподключен с новым сокетом ${newSocket.id}. Старый сокет: ${oldSocketId}. Активных игроков: ${this.playerCount}.`); const reconnectLogMessage = `🔌 Игрок ${reconnectedPlayerName} снова в игре!`; this.addToLog(reconnectLogMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); const opponentRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const oCharKey = this.gameState?.[opponentRoleKey]?.characterKey; const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; const logForReconnectedPlayer = this.consumeLogBuffer(); newSocket.emit('gameStarted', { // Используем 'gameStarted' для восстановления, как ожидает клиент gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState, playerBaseStats: pData.baseStats, opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null}, playerAbilities: pData.abilities, opponentAbilities: oData?.abilities || [], log: logForReconnectedPlayer, clientConfig: { ...GAME_CONFIG } }); const otherPlayerSocket = Object.values(this.players).find(p => p.id !== playerIdRole && p.socket && p.socket.connected && !p.isTemporarilyDisconnected )?.socket; if (otherPlayerSocket) { otherPlayerSocket.emit('playerReconnected', { playerId: playerIdRole, playerName: reconnectedPlayerName }); // Логи, которые могли накопиться для другого игрока (например, сообщение о реконнекте), уйдут со следующим broadcastGameStateUpdate } if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver) { console.log(`[GameInstance ${this.id}] Игра возобновлена после переподключения ${identifier} (роль: ${playerIdRole}). Отправка gameStateUpdate всем и перезапуск таймера.`); this.broadcastGameStateUpdate(); // Отправит оставшиеся логи const currentTurnPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const otherPlayerEntry = Object.values(this.players).find(p => p.id !== playerIdRole); // Проверяем другого игрока в целом // Таймер запускаем, если сейчас ход реконнектнувшегося ИЛИ если другой игрок активен (не isTemporarilyDisconnected) if (currentTurnPlayerRole === playerIdRole || (otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected)) { this.turnTimer.start(this.gameState.isPlayerTurn, (this.mode === 'ai' && !this.gameState.isPlayerTurn)); } else { console.log(`[GameInstance ${this.id}] Другой игрок (${otherPlayerEntry?.id}) отключен, таймер хода не запускается после реконнекта ${playerIdRole}.`); } } return true; } else if (playerEntry && !playerEntry.isTemporarilyDisconnected) { console.warn(`[GameInstance ${this.id}] Игрок ${identifier} (роль: ${playerIdRole}) пытается переподключиться, но не был помечен как отключенный.`); if (playerEntry.socket.id !== newSocket.id) { newSocket.emit('gameError', {message: "Вы уже активно подключены к этой игре."}); } return false; } else { console.warn(`[GameInstance ${this.id}] Не удалось найти игрока ${identifier} (роль: ${playerIdRole}) для переподключения, или он не был помечен как отключенный.`); newSocket.emit('gameError', { message: 'Не удалось восстановить сессию в этой игре.'}); return false; } } clearReconnectTimer(playerIdRole) { if (this.reconnectTimers[playerIdRole]) { clearTimeout(this.reconnectTimers[playerIdRole]); delete this.reconnectTimers[playerIdRole]; console.log(`[GameInstance ${this.id}] Таймер реконнекта для роли ${playerIdRole} очищен.`); } } clearAllReconnectTimers() { console.log(`[GameInstance ${this.id}] Очистка ВСЕХ таймеров реконнекта.`); for (const roleId in this.reconnectTimers) { this.clearReconnectTimer(roleId); } } isGameEffectivelyPaused() { if (this.mode === 'pvp') { // Игра на паузе, если хотя бы один из ДВУХ ожидаемых игроков временно отключен const player1 = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); const player2 = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID); return (player1?.isTemporarilyDisconnected || false) || (player2?.isTemporarilyDisconnected || false); } else if (this.mode === 'ai') { const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); return humanPlayer?.isTemporarilyDisconnected ?? (this.playerCount === 0 && Object.keys(this.players).length > 0); } return false; // По умолчанию не на паузе } initializeGame() { console.log(`[GameInstance ${this.id}] Инициализация состояния игры. Режим: ${this.mode}. Активных игроков: ${this.playerCount}. Всего записей в players: ${Object.keys(this.players).length}`); if (this.mode === 'ai' && this.playerCount === 1) { const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); this.playerCharacterKey = p1Info?.chosenCharacterKey || 'elena'; this.opponentCharacterKey = 'balard'; } else if (this.mode === 'pvp') { const p1Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); const p2Info = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected); if (p1Info) this.playerCharacterKey = p1Info.chosenCharacterKey; else this.playerCharacterKey = null; if (p2Info) this.opponentCharacterKey = p2Info.chosenCharacterKey; else this.opponentCharacterKey = null; if (this.playerCount === 1 && p1Info && !p2Info) { // this.opponentCharacterKey остается null } else if (this.playerCount === 2 && (!p1Info || !p2Info)) { console.error(`[GameInstance ${this.id}] Ошибка инициализации PvP: playerCount=2, но один из игроков не найден как активный.`); return false; } else if (this.playerCount < 2 && !p1Info) { console.log(`[GameInstance ${this.id}] Инициализация PvP без активного первого игрока. playerCharacterKey будет ${this.playerCharacterKey}.`); } } else { console.error(`[GameInstance ${this.id}] Некорректное состояние для инициализации! Активных: ${this.playerCount}`); return false; } const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null; let opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null; const isOpponentDefined = !!this.opponentCharacterKey; if (!playerData && (this.mode === 'ai' || (this.mode === 'pvp' && this.playerCount > 0))) { this._handleCriticalError('init_player_data_fail', 'Ошибка загрузки данных основного игрока при инициализации.'); return false; } if (isOpponentDefined && !opponentData) { this._handleCriticalError('init_opponent_data_fail', 'Ошибка загрузки данных оппонента при инициализации.'); return false; } this.gameState = { player: playerData ? this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities) : this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'Ресурс', attackPower: 0, characterKey: null }, []), opponent: isOpponentDefined && opponentData ? this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) : this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание игрока 2...', 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): ${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 => { 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.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Попытка старта игры, но она на паузе из-за дисконнекта. Старт отложен.`); return; } 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; } const initialLog = this.consumeLogBuffer(); Object.values(this.players).forEach(playerInfo => { if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) { 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: [...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(); 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.isGameEffectivelyPaused()) { console.warn(`[GameInstance ${this.id}] Действие от сокета ${requestingSocketId}, но игра на паузе. Действие отклонено.`); const playerInfo = this.players[requestingSocketId]; if (playerInfo?.socket) playerInfo.socket.emit('gameError', {message: "Действие невозможно: другой игрок отключен."}); return; } 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()) { return; } if (actionValid) { this.broadcastLogUpdate(); 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.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Попытка сменить ход, но игра на паузе. Смена хода отложена.`); return; } 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()) { 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.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Попытка хода AI, но игра на паузе. Ход AI отложен.`); return; } 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; } this.turnTimer.clear(); 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()) { return; } this.broadcastLogUpdate(); 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); } if (this.checkGameOver()) { return; } this.broadcastLogUpdate(); setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } checkGameOver() { if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true; if (this.mode === 'pvp' && (!this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey)) { return false; } if (this.mode === 'ai' && !this.gameState.player?.characterKey) return false; const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode); if (gameOverResult.isOver) { this.gameState.isGameOver = true; 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 && (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(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) { if (this.gameState && !this.gameState.isGameOver) { this.gameState.isGameOver = true; this.turnTimer.clear(); this.clearAllReconnectTimers(); const actualWinnerRole = winnerIfAny !== null ? winnerIfAny : (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID); const winnerExists = Object.values(this.players).some(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected) || (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID && this.gameState.opponent?.characterKey); const result = gameLogic.getGameOverResult( this.gameState, GAME_CONFIG, this.mode, reason, winnerExists ? actualWinnerRole : null, disconnectedPlayerRole ); this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); console.log(`[GameInstance ${this.id}] Игра завершена из-за дисконнекта/ухода. Причина: ${reason}. Победитель: ${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); } else if (this.gameState && this.gameState.isGameOver) { console.log(`[GameInstance ${this.id}] Попытка завершить игру из-за дисконнекта, но она уже завершена.`); } } // --- НАЧАЛО ИЗМЕНЕНИЯ --- playerDidSurrender(surrenderingPlayerIdentifier) { console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`); if (!this.gameState || this.gameState.isGameOver) { console.warn(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} попытался сдаться, но игра неактивна или уже завершена.`); return; } if (this.mode !== 'pvp') { console.log(`[GameInstance ${this.id}] Игрок ${surrenderingPlayerIdentifier} сдался в не-PvP игре. Просто завершаем игру, если это AI режим и игрок один.`); if (this.mode === 'ai') { const playerInfo = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier); if (playerInfo) { this.endGameDueToDisconnect(playerInfo.id, playerInfo.chosenCharacterKey, "player_left_ai_game"); } else { this.gameManager._cleanupGame(this.id, "surrender_ai_player_not_found"); } } return; } const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier); if (!surrenderedPlayerEntry) { console.error(`[GameInstance ${this.id}] Не найден игрок с identifier ${surrenderingPlayerIdentifier} для обработки сдачи.`); return; } const surrenderedPlayerRole = surrenderedPlayerEntry.id; const surrenderedPlayerName = this.gameState[surrenderedPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey || `Игрок (ID: ${surrenderingPlayerIdentifier})`; const surrenderedPlayerCharKey = this.gameState[surrenderedPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey; const winnerRole = surrenderedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const winnerName = this.gameState[winnerRole]?.name || `Оппонент (Роль: ${winnerRole})`; this.gameState.isGameOver = true; this.turnTimer.clear(); this.clearAllReconnectTimers(); // Также очищаем таймеры реконнекта const surrenderMessage = `🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`; this.addToLog(surrenderMessage, GAME_CONFIG.LOG_TYPE_SYSTEM); console.log(`[GameInstance ${this.id}] ${surrenderMessage}`); const reasonForGameOver = "player_surrendered"; console.log(`[GameInstance ${this.id}] Игра ${this.id} завершена из-за сдачи игрока ${surrenderedPlayerName} (роль: ${surrenderedPlayerRole}). Победитель: ${winnerName} (роль: ${winnerRole}).`); this.io.to(this.id).emit('gameOver', { winnerId: winnerRole, reason: reasonForGameOver, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: surrenderedPlayerCharKey }); // Вызываем cleanup в GameManager, чтобы удалить игру из активных списков if (this.gameManager && typeof this.gameManager._cleanupGame === 'function') { this.gameManager._cleanupGame(this.id, reasonForGameOver); } else { console.error(`[GameInstance ${this.id}] CRITICAL: gameManager or _cleanupGame method not found after surrender!`); } } // --- КОНЕЦ ИЗМЕНЕНИЯ --- handleTurnTimeout() { if (!this.gameState || this.gameState.isGameOver) return; 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) { this._handleCriticalError('timeout_winner_undefined', `Таймаут, но победитель (${winnerPlayerRole}) не определен.`); return; } this.gameState.isGameOver = true; this.clearAllReconnectTimers(); 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.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' }); 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; const logEntry = { message, type, timestamp: Date.now() }; this.logBuffer.push(logEntry); } consumeLogBuffer() { const logs = [...this.logBuffer]; this.logBuffer = []; return logs; } broadcastGameStateUpdate() { if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Попытка broadcastGameStateUpdate, но игра на паузе. Обновление не отправлено.`); return; } if (!this.gameState) return; const logsToSend = this.consumeLogBuffer(); this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: logsToSend }); } broadcastLogUpdate() { if (this.logBuffer.length > 0) { const logsToSend = this.consumeLogBuffer(); this.io.to(this.id).emit('logUpdate', { log: logsToSend }); } } } module.exports = GameInstance;