// /server_modules/gameInstance.js const GAME_CONFIG = require('./config'); const gameData = require('./data'); const serverGameLogic = require('./gameLogic'); class GameInstance { constructor(gameId, io, mode = 'ai') { this.id = gameId; this.io = io; this.mode = mode; this.players = {}; // { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest' } } this.playerSockets = {}; // { 'player': socketObject, 'opponent': socketObject } this.playerCount = 0; this.gameState = null; this.aiOpponent = (mode === 'ai'); this.logBuffer = []; this.restartVotes = new Set(); this.playerCharacterKey = null; this.opponentCharacterKey = null; this.ownerUserId = null; // userId создателя игры } addPlayer(socket, chosenCharacterKey = 'elena') { // Проверка, не пытается ли игрок присоединиться к игре, в которой он уже есть if (this.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' }); console.warn(`[Game ${this.id}] Игрок ${socket.id} попытался присоединиться к игре, в которой уже состоит.`); return false; } if (this.playerCount >= 2) { socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); return false; } let assignedPlayerId; let actualCharacterKey; if (this.mode === 'ai') { if (this.playerCount > 0) { // В AI игру может войти только один реальный игрок socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); return false; } assignedPlayerId = GAME_CONFIG.PLAYER_ID; actualCharacterKey = 'elena'; if (socket.userData?.userId) { this.ownerUserId = socket.userData.userId; } } else { // PvP if (this.playerCount === 0) { // Первый игрок PvP assignedPlayerId = GAME_CONFIG.PLAYER_ID; actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena'; if (socket.userData?.userId) { this.ownerUserId = socket.userData.userId; } } else { // Второй игрок PvP assignedPlayerId = GAME_CONFIG.OPPONENT_ID; const firstPlayerInfo = Object.values(this.players)[0]; // Информация о первом игроке actualCharacterKey = (firstPlayerInfo.chosenCharacterKey === 'elena') ? 'almagest' : 'elena'; } } this.players[socket.id] = { id: assignedPlayerId, socket: socket, chosenCharacterKey: actualCharacterKey }; this.playerSockets[assignedPlayerId] = socket; this.playerCount++; socket.join(this.id); const characterData = this._getCharacterBaseData(actualCharacterKey); console.log(`[Game ${this.id}] Игрок ${socket.id} (userId: ${socket.userData?.userId || 'N/A'}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerUserId || 'N/A'}`); if (this.mode === 'pvp' && this.playerCount < 2) { socket.emit('waitingForOpponent'); } // Если игра готова к старту (2 игрока в PvP, или 1 в AI) if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) { this.initializeGame(); if (this.gameState) { this.startGame(); } else { // Ошибка инициализации уже должна была быть залогирована и отправлена клиенту console.error(`[Game ${this.id}] Не удалось запустить игру, так как gameState не был инициализирован.`); } } return true; } removePlayer(socketId) { const playerInfo = this.players[socketId]; if (playerInfo) { const playerRole = playerInfo.id; let characterKeyToRemove = playerInfo.chosenCharacterKey; const userIdOfLeavingPlayer = playerInfo.socket?.userData?.userId; if (this.mode === 'ai' && playerRole === GAME_CONFIG.OPPONENT_ID) { // AI оппонент не имеет chosenCharacterKey в this.players characterKeyToRemove = 'balard'; } else if (!characterKeyToRemove && this.gameState) { // Фоллбэк, если ключ не был в playerInfo characterKeyToRemove = (playerRole === GAME_CONFIG.PLAYER_ID) ? this.gameState.player?.characterKey : this.gameState.opponent?.characterKey; } const characterData = this._getCharacterBaseData(characterKeyToRemove); console.log(`[Game ${this.id}] Игрок ${socketId} (userId: ${userIdOfLeavingPlayer || 'N/A'}) (${characterData?.name || 'Неизвестно'}, роль: ${playerRole}, персонаж: ${characterKeyToRemove || 'N/A'}) покинул игру.`); if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) { delete this.playerSockets[playerRole]; } delete this.players[socketId]; this.playerCount--; if (this.mode === 'pvp' && this.ownerUserId === userIdOfLeavingPlayer && this.playerCount === 1) { const remainingPlayerSocketId = Object.keys(this.players)[0]; const remainingPlayerSocket = this.players[remainingPlayerSocketId]?.socket; this.ownerUserId = remainingPlayerSocket?.userData?.userId || null; console.log(`[Game ${this.id}] Owner left. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`); } else if (this.playerCount === 0) { this.ownerUserId = null; } if (this.gameState && !this.gameState.isGameOver) { this.endGameDueToDisconnect(playerRole, characterKeyToRemove); } } } endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey) { if (this.gameState && !this.gameState.isGameOver) { this.gameState.isGameOver = true; const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const disconnectedCharacterData = this._getCharacterBaseData(disconnectedCharacterKey); this.addToLog(`Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} покинул игру.`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.io.to(this.id).emit('opponentDisconnected', { disconnectedPlayerId: disconnectedPlayerRole }); this.io.to(this.id).emit('gameOver', { winnerId: (this.mode === 'pvp' || winnerRole === GAME_CONFIG.OPPONENT_ID) ? winnerRole : GAME_CONFIG.OPPONENT_ID, reason: 'opponent_disconnected', finalGameState: this.gameState, log: this.consumeLogBuffer() }); } } initializeGame() { console.log(`[Game ${this.id}] Initializing game state for (re)start... Mode: ${this.mode}`); if (this.mode === 'ai') { this.playerCharacterKey = 'elena'; this.opponentCharacterKey = 'balard'; } else { // pvp const playerSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); const opponentSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID); this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Игрок 1 (слот 'player') if (this.playerCount === 2 && opponentSocketInfo) { // Если есть второй игрок (слот 'opponent') this.opponentCharacterKey = opponentSocketInfo.chosenCharacterKey; // Убедимся, что персонажи разные if (this.playerCharacterKey === this.opponentCharacterKey) { this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; opponentSocketInfo.chosenCharacterKey = this.opponentCharacterKey; // Обновляем и в информации о сокете console.warn(`[Game ${this.id}] Corrected character selection in PvP. Opponent for slot ${GAME_CONFIG.OPPONENT_ID} is now ${this.opponentCharacterKey}`); } } else if (this.playerCount === 1) { // Только один игрок в PvP this.opponentCharacterKey = null; // Оппонент еще не определен } else { console.error(`[Game ${this.id}] Unexpected playerCount (${this.playerCount}) or missing socketInfo during PvP character key assignment.`); this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; // Фоллбэк } } console.log(`[Game ${this.id}] Finalizing characters - Player Slot: ${this.playerCharacterKey}, Opponent Slot: ${this.opponentCharacterKey || 'N/A'}`); const playerBase = this._getCharacterBaseData(this.playerCharacterKey); const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey); let opponentBase = null; let opponentAbilities = null; if (this.opponentCharacterKey) { opponentBase = this._getCharacterBaseData(this.opponentCharacterKey); opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey); } const isReadyForFullGameState = (this.mode === 'ai') || (this.mode === 'pvp' && this.playerCount === 2 && opponentBase && opponentAbilities); if (!playerBase || !playerAbilities || (!isReadyForFullGameState && !(this.mode === 'pvp' && this.playerCount === 1)) ) { console.error(`[Game ${this.id}] CRITICAL ERROR: Failed to load necessary character data for initialization! Player: ${this.playerCharacterKey}, Opponent: ${this.opponentCharacterKey}, PlayerCount: ${this.playerCount}, Mode: ${this.mode}`); this.logBuffer = []; this.addToLog('Критическая ошибка сервера при инициализации персонажей!', GAME_CONFIG.LOG_TYPE_SYSTEM); this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры.' }); this.gameState = null; return; } this.gameState = { player: { id: GAME_CONFIG.PLAYER_ID, characterKey: this.playerCharacterKey, name: playerBase.name, currentHp: playerBase.maxHp, maxHp: playerBase.maxHp, currentResource: playerBase.maxResource, maxResource: playerBase.maxResource, resourceName: playerBase.resourceName, attackPower: playerBase.attackPower, isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {} }, opponent: { id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey, name: opponentBase?.name || 'Ожидание...', currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1, currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0, resourceName: opponentBase?.resourceName || 'Неизвестно', attackPower: opponentBase?.attackPower || 0, isBlocking: false, activeEffects: [], silenceCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined, manaDrainCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined, abilityCooldowns: {} }, isPlayerTurn: Math.random() < 0.5, isGameOver: false, turnNumber: 1, gameMode: this.mode }; playerAbilities.forEach(ability => { if (typeof ability.cooldown === 'number' && ability.cooldown > 0) this.gameState.player.abilityCooldowns[ability.id] = 0; }); if (opponentAbilities) { opponentAbilities.forEach(ability => { let cd = 0; if (ability.cooldown) cd = ability.cooldown; else if (this.opponentCharacterKey === 'balard') { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) cd = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') cd = ability.internalCooldownValue; } if (cd > 0) this.gameState.opponent.abilityCooldowns[ability.id] = 0; }); } this.restartVotes.clear(); const isFullGameReadyForLog = (this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2 && this.opponentCharacterKey); const isRestart = this.logBuffer.length > 0 && isFullGameReadyForLog; this.logBuffer = []; if (isFullGameReadyForLog) { this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM); } console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState.isGameOver}. First turn: ${this.gameState.isPlayerTurn ? this.gameState.player.name : (this.gameState.opponent?.name || 'Оппонент')}`); } startGame() { if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание...') { if (this.mode === 'pvp' && this.playerCount === 1 && !this.opponentCharacterKey) { console.log(`[Game ${this.id}] startGame: Waiting for opponent in PvP game.`); } else if (!this.gameState) { console.error(`[Game ${this.id}] Game cannot start: gameState is null.`); } else { console.warn(`[Game ${this.id}] Game not fully ready to start. OpponentKey: ${this.opponentCharacterKey}, OpponentName: ${this.gameState.opponent.name}, PlayerCount: ${this.playerCount}`); } return; } console.log(`[Game ${this.id}] Starting game. Broadcasting 'gameStarted' to players. isGameOver: ${this.gameState.isGameOver}`); const playerCharData = this._getCharacterData(this.playerCharacterKey); const opponentCharData = this._getCharacterData(this.opponentCharacterKey); if (!playerCharData || !opponentCharData) { console.error(`[Game ${this.id}] CRITICAL ERROR: startGame - Failed to load character data! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`); this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при старте игры (данные персонажей).' }); return; } Object.values(this.players).forEach(pInfo => { let dataForThisClient; if (pInfo.id === GAME_CONFIG.PLAYER_ID) { dataForThisClient = { gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState, playerBaseStats: playerCharData.baseStats, opponentBaseStats: opponentCharData.baseStats, playerAbilities: playerCharData.abilities, opponentAbilities: opponentCharData.abilities, log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } }; } else { dataForThisClient = { gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState, playerBaseStats: opponentCharData.baseStats, opponentBaseStats: playerCharData.baseStats, playerAbilities: opponentCharData.abilities, opponentAbilities: playerCharData.abilities, log: [], clientConfig: { ...GAME_CONFIG } }; } pInfo.socket.emit('gameStarted', dataForThisClient); }); const firstTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name; this.addToLog(`--- ${firstTurnName} ходит первым! ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); if (!this.gameState.isPlayerTurn) { if (this.aiOpponent && this.opponentCharacterKey === 'balard') { setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } else { this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID }); } } else { this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID }); } } handleVoteRestart(requestingSocketId) { if (!this.gameState || !this.gameState.isGameOver) { const playerSocket = this.players[requestingSocketId]?.socket || this.io.sockets.sockets.get(requestingSocketId); if(playerSocket) playerSocket.emit('gameError', {message: "Нельзя рестартовать игру, которая не завершена."}); return; } if (!this.players[requestingSocketId]) return; this.restartVotes.add(requestingSocketId); const voterInfo = this.players[requestingSocketId]; const voterCharacterKey = voterInfo.id === GAME_CONFIG.PLAYER_ID ? this.gameState.player.characterKey : this.gameState.opponent.characterKey; const voterCharacterData = this._getCharacterBaseData(voterCharacterKey); this.addToLog(`Игрок ${voterCharacterData?.name || 'Неизвестный'} (${voterInfo.id}) голосует за рестарт.`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.broadcastLogUpdate(); const requiredVotes = this.playerCount > 0 ? this.playerCount : 1; if (this.restartVotes.size >= requiredVotes) { this.initializeGame(); if (this.gameState) this.startGame(); else console.error(`[Game ${this.id}] Failed to restart: gameState is null after re-initialization.`); } else if (this.mode === 'pvp') { this.io.to(this.id).emit('waitingForRestartVote', { voterCharacterName: voterCharacterData?.name || 'Неизвестный', voterRole: voterInfo.id, votesNeeded: requiredVotes - this.restartVotes.size }); } } processPlayerAction(requestingSocketId, actionData) { if (!this.gameState || this.gameState.isGameOver) return; const actingPlayerInfo = this.players[requestingSocketId]; if (!actingPlayerInfo) { console.error(`[Game ${this.id}] Action from unknown socket ${requestingSocketId}`); return; } const actingPlayerRole = actingPlayerInfo.id; const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) || (!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID); if (!isCorrectTurn) { actingPlayerInfo.socket.emit('gameError', { message: "Сейчас не ваш ход!" }); return; } const attackerState = this.gameState[actingPlayerRole]; const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const defenderState = this.gameState[defenderRole]; const attackerData = this._getCharacterData(attackerState.characterKey); const defenderData = this._getCharacterData(defenderState.characterKey); if (!attackerData || !defenderData) { this.addToLog('Критическая ошибка сервера при обработке действия (данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM); this.broadcastLogUpdate(); return; } const attackerBaseStats = attackerData.baseStats; const defenderBaseStats = defenderData.baseStats; const attackerAbilities = attackerData.abilities; let actionValid = true; if (actionData.actionType === 'attack') { let taunt = ""; if (attackerState.characterKey === 'elena') { taunt = serverGameLogic.getElenaTaunt('playerBasicAttack', {}, GAME_CONFIG, gameData, this.gameState); } const attackBuffAbilityId = attackerState.characterKey === 'elena' ? GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH : (attackerState.characterKey === 'almagest' ? GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK : null); let attackBuffEffect = null; if (attackBuffAbilityId) { attackBuffEffect = attackerState.activeEffects.find(eff => eff.id === attackBuffAbilityId); } if (attackerState.characterKey === 'elena' && taunt && taunt !== "(Молчание)") { this.addToLog(`${attackerState.name} атакует: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); } else { this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); } if (attackBuffEffect && !attackBuffEffect.justCast) { this.addToLog(`✨ Эффект "${attackBuffEffect.name}" активен! Атака восстановит ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_EFFECT); } serverGameLogic.performAttack( attackerState, defenderState, attackerBaseStats, defenderBaseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData ); if (attackBuffEffect) { const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerBaseStats.maxResource - attackerState.currentResource); if (actualRegen > 0) { attackerState.currentResource += actualRegen; this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${attackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL); } // Эффект НЕ удаляется здесь для многоразового действия } } else if (actionData.actionType === 'ability' && actionData.abilityId) { const ability = attackerAbilities.find(ab => ab.id === actionData.abilityId); if (!ability) { actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." }); return; } if (attackerState.currentResource < ability.cost) { this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (actionValid && attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) { this.addToLog(`"${ability.name}" еще не готова (КД: ${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (actionValid && attackerState.characterKey === 'balard') { if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && (attackerState.silenceCooldownTurns > 0 || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0))) { this.addToLog(`"${ability.name}" еще не готова (спец. КД или общий КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && (attackerState.manaDrainCooldownTurns > 0 || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0))) { this.addToLog(`"${ability.name}" еще не готова (спец. КД или общий КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } } if (actionValid && ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id)) { this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } const isDebuffAbility = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF; if (actionValid && isDebuffAbility) { if (defenderState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } } if (actionValid) { attackerState.currentResource -= ability.cost; let baseCooldown = 0; if (ability.cooldown) baseCooldown = ability.cooldown; else if (attackerState.characterKey === 'balard') { if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; } else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; } else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; } } if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1; let logMessage = `${attackerState.name} колдует "${ability.name}" (-${ability.cost} ${attackerState.resourceName})`; if (attackerState.characterKey === 'elena') { const taunt = serverGameLogic.getElenaTaunt('playerActionCast', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState); if (taunt && taunt !== "(Молчание)") logMessage += `: "${taunt}"`; } const logType = ability.type === GAME_CONFIG.ACTION_TYPE_HEAL ? GAME_CONFIG.LOG_TYPE_HEAL : ability.type === GAME_CONFIG.ACTION_TYPE_DAMAGE ? GAME_CONFIG.LOG_TYPE_DAMAGE : GAME_CONFIG.LOG_TYPE_EFFECT; this.addToLog(logMessage, logType); const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF) ? attackerState : defenderState; const targetBaseStatsForAbility = (targetForAbility.id === defenderState.id ? defenderBaseStats : attackerBaseStats); serverGameLogic.applyAbilityEffect(ability, attackerState, targetForAbility, attackerBaseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData); } } else { actionValid = false; } if (!actionValid) { this.broadcastLogUpdate(); return; } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); } switchTurn() { if (!this.gameState || this.gameState.isGameOver) return; const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const endingTurnActorState = this.gameState[endingTurnActorRole]; const endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey); if (!endingTurnCharacterData) { console.error(`SwitchTurn Error: No char data for ${endingTurnActorState.characterKey}`); return; } serverGameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnCharacterData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData); serverGameLogic.updateBlockingStatus(this.gameState.player); serverGameLogic.updateBlockingStatus(this.gameState.opponent); if (endingTurnActorState.abilityCooldowns) { serverGameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnCharacterData.abilities, endingTurnActorState.name, this.addToLog.bind(this)); } if (endingTurnActorState.characterKey === 'balard') { if (endingTurnActorState.silenceCooldownTurns !== undefined && endingTurnActorState.silenceCooldownTurns > 0) endingTurnActorState.silenceCooldownTurns--; if (endingTurnActorState.manaDrainCooldownTurns !== undefined && endingTurnActorState.manaDrainCooldownTurns > 0) endingTurnActorState.manaDrainCooldownTurns--; } if (endingTurnActorRole === GAME_CONFIG.OPPONENT_ID) { const playerStateInGame = this.gameState.player; if (playerStateInGame.disabledAbilities?.length > 0) { const playerCharAbilities = this._getCharacterAbilities(playerStateInGame.characterKey); if (playerCharAbilities) serverGameLogic.processDisabledAbilities(playerStateInGame.disabledAbilities, playerCharAbilities, playerStateInGame.name, this.addToLog.bind(this)); } } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; if (this.gameState.isPlayerTurn) this.gameState.turnNumber++; const currentTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent; this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.broadcastGameStateUpdate(); if (!this.gameState.isPlayerTurn) { if (this.aiOpponent && this.opponentCharacterKey === 'balard') { setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); } else { this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID }); } } else { this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID }); } } processAiTurn() { if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.opponentCharacterKey !== 'balard') { if(!this.gameState || this.gameState.isGameOver) return; return; } const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this)); const attackerState = this.gameState.opponent; const defenderState = this.gameState.player; const attackerData = this._getCharacterData('balard'); const defenderData = this._getCharacterData(defenderState.characterKey); if (!attackerData || !defenderData) { this.switchTurn(); return; } let actionValid = true; if (aiDecision.actionType === 'attack') { this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData); } else if (aiDecision.actionType === 'ability' && aiDecision.ability) { const ability = aiDecision.ability; if (attackerState.currentResource < ability.cost || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) || (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) || (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0) ) { actionValid = false; this.addToLog(`AI ${attackerState.name} не смог применить "${ability.name}" (ресурс/КД).`, GAME_CONFIG.LOG_TYPE_INFO); } if (actionValid) { attackerState.currentResource -= ability.cost; let baseCooldown = 0; if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; } else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; } else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; } if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1; this.addToLog(`${attackerState.name} применяет "${ability.name}"...`, GAME_CONFIG.LOG_TYPE_EFFECT); const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL) ? attackerState : defenderState; const targetBaseStatsForAbility = (targetForAbility.id === defenderState.id ? defenderData.baseStats : attackerData.baseStats); serverGameLogic.applyAbilityEffect(ability, attackerState, targetForAbility, attackerData.baseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData); } } else if (aiDecision.actionType === 'pass') { if (aiDecision.logMessage) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type); else this.addToLog(`${attackerState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO); } else actionValid = false; if (!actionValid) this.addToLog(`${attackerState.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO); if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } this.switchTurn(); } checkGameOver() { if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true; const playerState = this.gameState.player; const opponentState = this.gameState.opponent; if (!playerState || !opponentState) return false; const playerDead = playerState.currentHp <= 0; const opponentDead = opponentState.currentHp <= 0; if (playerDead || opponentDead) { this.gameState.isGameOver = true; const winnerRole = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const loserRole = opponentDead ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const winnerState = this.gameState[winnerRole]; const loserState = this.gameState[loserRole]; const winnerName = winnerState?.name || (winnerRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник"); const loserName = loserState?.name || (loserRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник"); this.addToLog(`ПОБЕДА! ${winnerName} одолел(а) ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM); if (winnerState?.characterKey === 'elena') { const taunt = serverGameLogic.getElenaTaunt('opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState); if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); if (loserState?.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM); else if (loserState?.characterKey === 'almagest') this.addToLog(`Елена одержала победу над темной волшебницей ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM); } this.io.to(this.id).emit('gameOver', { winnerId: winnerRole, reason: `${loserName} побежден(а)`, finalGameState: this.gameState, log: this.consumeLogBuffer() }); return true; } return false; } addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() }); } consumeLogBuffer() { const logs = [...this.logBuffer]; this.logBuffer = []; return logs; } broadcastGameStateUpdate() { if (!this.gameState) return; this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() }); } broadcastLogUpdate() { if (this.logBuffer.length > 0) this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });} _getCharacterData(key) { if(!key) return null; switch (key) { case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities }; case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; default: console.error(`_getCharacterData: Unknown character key "${key}"`); return null; }} _getCharacterBaseData(key) { if(!key) return null; switch (key) { case 'elena': return gameData.playerBaseStats; case 'balard': return gameData.opponentBaseStats; case 'almagest': return gameData.almagestBaseStats; default: console.error(`_getCharacterBaseData: Unknown character key "${key}"`); return null; }} _getCharacterAbilities(key) { if(!key) return null; switch (key) { case 'elena': return gameData.playerAbilities; case 'balard': return gameData.opponentAbilities; case 'almagest': return gameData.almagestAbilities; default: console.error(`_getCharacterAbilities: Unknown character key "${key}"`); return null; }} } module.exports = GameInstance;