// /server_modules/gameLogic.js const GAME_CONFIG = require('./config'); // <--- ДОБАВЛЕНО // Этот файл содержит основную логику игры, адаптированную для сервера. // ... // --- Вспомогательные Функции --- /** * Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода. * @param {Array} effectsArray - Массив активных эффектов бойца. * @param {Object} ownerState - Состояние бойца (currentHp, currentResource и т.д.). * @param {Object} ownerBaseStats - Базовые статы бойца. * @param {String} ownerId - ID бойца ('player' или 'opponent'). * @param {Object} currentGameState - Полное состояние игры (для доступа к противнику, если нужно). * @param {Function} addToLogCallback - Функция для добавления сообщений в лог игры. * @param {Object} passed_GAME_CONFIG - Конфигурационный объект игры (переименовал, чтобы не конфликтовать с верхним GAME_CONFIG). * @returns {void} - Модифицирует effectsArray и ownerState напрямую. */ // В processEffects уже передается GAME_CONFIG, так что можно использовать его (или тот, что require выше) function processEffects(effectsArray, ownerState, ownerBaseStats, ownerId, currentGameState, addToLogCallback, passed_GAME_CONFIG) { if (!effectsArray) return; const ownerName = ownerBaseStats.name; const configToUse = passed_GAME_CONFIG || GAME_CONFIG; // Используем переданный, если есть, иначе из require let effectsToRemoveIndexes = []; for (let i = 0; i < effectsArray.length; i++) { const eff = effectsArray[i]; if (eff.id === 'fullSilenceByElena' && eff.power && typeof eff.power === 'number' && eff.power > 0) { const damage = eff.power; ownerState.currentHp = Math.max(0, ownerState.currentHp - damage); if (addToLogCallback) addToLogCallback(`😵 Эффект "${eff.name}" наносит ${damage} урона ${ownerName}!`, configToUse.LOG_TYPE_DAMAGE); // checkGameOverInternal теперь тоже должен получать configToUse и gameData if (checkGameOverInternal(currentGameState, configToUse, require('./data'))) { /* ... */ } } if (eff.id === ('effect_' + configToUse.ABILITY_ID_SEAL_OF_WEAKNESS) && ownerId === configToUse.OPPONENT_ID) { const resourceToBurn = eff.power; if (resourceToBurn > 0 && ownerState.currentResource > 0) { const actualBurn = Math.min(ownerState.currentResource, resourceToBurn); ownerState.currentResource = Math.max(0, ownerState.currentResource - actualBurn); if (addToLogCallback) addToLogCallback(`🔥 Эффект "${eff.name}" сжигает ${actualBurn} ${ownerBaseStats.resourceName} у ${ownerName}!`, configToUse.LOG_TYPE_EFFECT); } } if (eff.justCast) { eff.justCast = false; } else { eff.turnsLeft--; } if (eff.turnsLeft <= 0) { effectsToRemoveIndexes.push(i); let effectEndMessage = `Эффект "${eff.name}" на ${ownerName} закончился.`; if (addToLogCallback) addToLogCallback(effectEndMessage, configToUse.LOG_TYPE_EFFECT); if (eff.id === configToUse.ABILITY_ID_DEFENSE_AURA && eff.grantsBlock) { ownerState.isBlocking = false; } } } for (let i = effectsToRemoveIndexes.length - 1; i >= 0; i--) { effectsArray.splice(effectsToRemoveIndexes[i], 1); } } /** Обрабатывает отсчет для отключенных (заглушенных) способностей Елены. */ function processDisabledAbilities(disabledAbilitiesArray, addToLogCallback /*, configToUse - если нужен */) { if (!disabledAbilitiesArray || disabledAbilitiesArray.length === 0) return; const localGameData = require('./data'); // Загружаем gameData здесь, если он нужен локально const stillDisabled = []; disabledAbilitiesArray.forEach(dis => { dis.turnsLeft--; if (dis.turnsLeft > 0) { stillDisabled.push(dis); } else { if (addToLogCallback) { const ability = localGameData.playerAbilities.find(ab => ab.id === dis.abilityId); if (ability) addToLogCallback(`Способность Елены "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO); // Используем GAME_CONFIG из require } } }); disabledAbilitiesArray.length = 0; disabledAbilitiesArray.push(...stillDisabled); } /** Обрабатывает отсчет кулдаунов для способностей игрока (или Баларда-игрока). */ function processPlayerAbilityCooldowns(cooldownsObject, addToLogCallback, ownerName = "Елены", abilitiesList) { if (!cooldownsObject || !abilitiesList) { return; } for (const abilityId in cooldownsObject) { if (cooldownsObject.hasOwnProperty(abilityId) && cooldownsObject[abilityId] > 0) { cooldownsObject[abilityId]--; if (cooldownsObject[abilityId] === 0) { const ability = abilitiesList.find(ab => ab.id === abilityId); if (ability && addToLogCallback) { // GAME_CONFIG.LOG_TYPE_INFO теперь доступен из require('./config') вверху файла addToLogCallback(`Способность ${ownerName} "${ability.name}" снова готова!`, GAME_CONFIG.LOG_TYPE_INFO); } } } } } /** Обновляет статус 'isBlocking' на основе активных эффектов. */ function updateBlockingStatus(fighterState) { if (!fighterState) return; fighterState.isBlocking = fighterState.activeEffects.some(eff => eff.grantsBlock && eff.turnsLeft > 0); } /** Выбирает подходящую насмешку для Елены. */ // GAME_CONFIG и gameData передаются, так что используем их function getElenaTaunt(trigger, context = {}, passed_GAME_CONFIG, passed_gameData, currentGameState) { const configToUse = passed_GAME_CONFIG || GAME_CONFIG; const gameDataToUse = passed_gameData || require('./data'); // Фоллбэк, если не передан const tauntSystem = gameDataToUse?.elenaTauntSystem; if (!tauntSystem || !currentGameState) { return "(Молчание)"; } let potentialTaunts = []; const opponentHpPerc = (currentGameState.opponent.currentHp / currentGameState.opponent.maxHp) * 100; const isOpponentLowHpForDomination = opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT; const isOpponentNearDefeat = opponentHpPerc < 20; // Можно вынести в configToUse if (trigger === 'opponentNearDefeatCheck' && isOpponentNearDefeat && tauntSystem.onBattleState?.opponentNearDefeat) { potentialTaunts = tauntSystem.onBattleState.opponentNearDefeat; } else if (trigger === 'opponentAction' && context.abilityId) { const actionResponses = tauntSystem.onOpponentAction?.[context.abilityId]; if (actionResponses) { if (typeof actionResponses === 'object' && !Array.isArray(actionResponses) && context.outcome && context.outcome in actionResponses) { potentialTaunts = actionResponses[context.outcome]; } else if (Array.isArray(actionResponses)) { potentialTaunts = actionResponses; } } } else if (trigger === 'opponentAttackBlocked' && tauntSystem.onOpponentAction?.attackBlocked) { potentialTaunts = tauntSystem.onOpponentAction.attackBlocked; } else if (trigger === 'opponentAttackHit' && tauntSystem.onOpponentAction?.attackHits) { potentialTaunts = tauntSystem.onOpponentAction.attackHits; } else if (trigger === 'playerActionCast' && context.abilityId && tauntSystem.onPlayerCast?.[context.abilityId]) { potentialTaunts = tauntSystem.onPlayerCast[context.abilityId]; } else if (trigger === 'playerBasicAttack') { if (isOpponentLowHpForDomination) { const pools = tauntSystem.base?.dominating || {}; potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ]; } else { potentialTaunts = tauntSystem.base?.mercifulAttack || []; } } else if (trigger === 'playerActionGeneral') { if (isOpponentLowHpForDomination) { const pools = tauntSystem.base?.dominating || {}; potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ]; } else { potentialTaunts = tauntSystem.base?.mercifulCast || []; } } else if (trigger === 'battleStart' && tauntSystem.onBattleState?.startMerciful) { potentialTaunts = tauntSystem.onBattleState.startMerciful; } if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) { if (isOpponentLowHpForDomination) { const pools = tauntSystem.base?.dominating || {}; potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ]; } else { potentialTaunts = [...(tauntSystem.base?.mercifulAttack || []), ...(tauntSystem.base?.mercifulCast || [])]; } } if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) { return "(Молчание)"; } return potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)] || "(Молчание)"; } // --- Основные Игровые Функции --- // performAttack, applyAbilityEffect, decideAiAction уже получают GAME_CONFIG и gameData как параметры // поэтому внутри них можно использовать переданные значения. /** * Выполняет базовую атаку. * @param {Object} attackerState Состояние атакующего. * @param {Object} defenderState Состояние защищающегося. * @param {Object} attackerBaseStats Базовые статы атакующего. * @param {Object} defenderBaseStats Базовые статы защищающегося. * @param {Object} currentGameState Полное состояние игры. * @param {Function} addToLogCallback Функция для логгирования. * @param {Object} passed_GAME_CONFIG Конфиг игры. */ function performAttack(attackerState, defenderState, attackerBaseStats, defenderBaseStats, currentGameState, addToLogCallback, passed_GAME_CONFIG) { const configToUse = passed_GAME_CONFIG || GAME_CONFIG; // Используем переданный или из require const localGameData = require('./data'); // Для доступа к gameData внутри, если нужно для таунтов let damage = Math.floor(attackerBaseStats.attackPower * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)); let wasBlocked = false; if (defenderState.isBlocking) { wasBlocked = true; damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION); let blockTaunt = ""; if (attackerState.id === configToUse.OPPONENT_ID && defenderState.id === configToUse.PLAYER_ID) { blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, localGameData, currentGameState); if (blockTaunt === "(Молчание)") blockTaunt = ""; else blockTaunt = ` (${blockTaunt})`; } if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку!${blockTaunt} Урон снижен до: ${damage}.`, configToUse.LOG_TYPE_BLOCK); } else { let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`; if (attackerState.id === configToUse.OPPONENT_ID && defenderState.id === configToUse.PLAYER_ID) { let hitTaunt = getElenaTaunt('opponentAttackHit', {}, configToUse, localGameData, currentGameState); if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`; } if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE); } defenderState.currentHp = Math.max(0, defenderState.currentHp - damage); // checkGameOverInternal(currentGameState, configToUse, localGameData); } /** * Применяет эффекты способности. * @param {Object} ability - Объект способности из gameData. * @param {Object} casterState - Состояние того, кто кастует. * @param {Object} targetState - Состояние цели. * @param {Object} casterBaseStats - Базовые статы кастера. * @param {Object} targetBaseStats - Базовые статы цели. * @param {Object} currentGameState - Полное состояние игры. * @param {Function} addToLogCallback - Функция для логгирования. * @param {Object} passed_GAME_CONFIG - Конфиг игры. */ function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, passed_GAME_CONFIG) { const configToUse = passed_GAME_CONFIG || GAME_CONFIG; const localGameData = require('./data'); // Для доступа к playerAbilities и т.д. switch (ability.type) { case configToUse.ACTION_TYPE_HEAL: const healAmount = Math.floor(ability.power * (configToUse.HEAL_VARIATION_MIN + Math.random() * configToUse.HEAL_VARIATION_RANGE)); const actualHeal = Math.min(healAmount, casterBaseStats.maxHp - casterState.currentHp); if (actualHeal > 0) { casterState.currentHp += actualHeal; if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL); } else { if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} уже имеет полное здоровье или эффект не дал лечения.`, configToUse.LOG_TYPE_INFO); } break; case configToUse.ACTION_TYPE_DAMAGE: let damage = Math.floor(ability.power * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)); if (targetState.isBlocking) { damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION); let blockTaunt = ""; if (casterState.id === configToUse.PLAYER_ID && targetState.id === configToUse.OPPONENT_ID) { // No specific taunt here } else if (casterState.id === configToUse.OPPONENT_ID && targetState.id === configToUse.PLAYER_ID) { blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, localGameData, currentGameState); if (blockTaunt === "(Молчание)") blockTaunt = ""; else blockTaunt = ` (${blockTaunt})`; } if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}"!${blockTaunt} Урон снижен до ${damage}.`, configToUse.LOG_TYPE_BLOCK); } targetState.currentHp = Math.max(0, targetState.currentHp - damage); if (addToLogCallback && !targetState.isBlocking) addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE); break; case configToUse.ACTION_TYPE_BUFF: if (!casterState.activeEffects.some(e => e.id === ability.id)) { let effectDescription = ability.description; if (typeof ability.descriptionFunction === 'function') { effectDescription = ability.descriptionFunction(configToUse, localGameData.opponentBaseStats); } casterState.activeEffects.push({ id: ability.id, name: ability.name, description: effectDescription, type: ability.type, turnsLeft: ability.duration, grantsBlock: !!ability.grantsBlock, justCast: !!ability.isDelayed }); if (ability.grantsBlock) updateBlockingStatus(casterState); } else { if (addToLogCallback) addToLogCallback(`Эффект "${ability.name}" уже активен на ${casterBaseStats.name}!`, configToUse.LOG_TYPE_INFO); } break; case configToUse.ACTION_TYPE_DISABLE: if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE) { const effectId = 'fullSilenceByElena'; if (!targetState.activeEffects.some(e => e.id === effectId)) { targetState.activeEffects.push({ id: effectId, name: ability.name, description: ability.description, type: ability.type, turnsLeft: ability.effectDuration, power: ability.power, isFullSilence: true }); if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}"! Способности ${targetBaseStats.name} заблокированы на ${ability.effectDuration} хода и он получает урон!`, configToUse.LOG_TYPE_EFFECT); } } else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE) { const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE; const silenceOutcome = success ? 'success' : 'fail'; const silenceTaunt = getElenaTaunt('opponentAction', { abilityId: configToUse.ABILITY_ID_BALARD_SILENCE, outcome: silenceOutcome }, configToUse, localGameData, currentGameState); if (success) { const availableAbilities = localGameData.playerAbilities.filter(pa => !targetState.disabledAbilities?.some(d => d.abilityId === pa.id) && !targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`) ); if (availableAbilities.length > 0) { const abilityToSilence = availableAbilities[Math.floor(Math.random() * availableAbilities.length)]; const turns = configToUse.SILENCE_DURATION; targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); const silenceEffectIdOnPlayer = `playerSilencedOn_${abilityToSilence.id}`; targetState.activeEffects.push({ id: silenceEffectIdOnPlayer, name: `Безмолвие: ${abilityToSilence.name}`, description: `Способность "${abilityToSilence.name}" временно недоступна.`, type: configToUse.ACTION_TYPE_DISABLE, turnsLeft: turns + 1 }); if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" Елены заблокировано! ${silenceTaunt}`, configToUse.LOG_TYPE_EFFECT); } else { if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить! ${silenceTaunt}`, configToUse.LOG_TYPE_INFO); } } else { if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась! ${silenceTaunt}`, configToUse.LOG_TYPE_INFO); } } break; case configToUse.ACTION_TYPE_DEBUFF: if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS) { const effectIdForDebuff = 'effect_' + ability.id; if (!targetState.activeEffects.some(e => e.id === effectIdForDebuff)) { let effectDescription = ability.description; if (typeof ability.descriptionFunction === 'function') { effectDescription = ability.descriptionFunction(configToUse, targetBaseStats); } targetState.activeEffects.push({ id: effectIdForDebuff, name: ability.name, description: effectDescription, type: configToUse.ACTION_TYPE_DEBUFF, sourceAbilityId: ability.id, turnsLeft: ability.effectDuration, power: ability.power, }); } } break; case 'drain': // Похищение Света Балардом let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0; if (ability.powerDamage > 0) { let baseDamageDrain = ability.powerDamage; if (targetState.isBlocking) { baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION); const blockDrainTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, localGameData, currentGameState); if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует часть урона от "${ability.name}"! Урон снижен до ${baseDamageDrain}. (${blockDrainTaunt})`, configToUse.LOG_TYPE_BLOCK); } damageDealtDrain = Math.max(0, baseDamageDrain); targetState.currentHp = Math.max(0, targetState.currentHp - damageDealtDrain); } const potentialDrain = ability.powerManaDrain; const actualDrain = Math.min(potentialDrain, targetState.currentResource); if (actualDrain > 0) { targetState.currentResource -= actualDrain; manaDrained = actualDrain; const potentialHeal = Math.floor(manaDrained * ability.powerHealthGainFactor); const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp); casterState.currentHp += actualHealGain; healthGained = actualHealGain; } const drainTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id }, configToUse, localGameData, currentGameState); let logMsgDrain = `⚡ ${casterBaseStats.name} применяет "${ability.name}"! `; if (damageDealtDrain > 0) logMsgDrain += `Наносит ${damageDealtDrain} урона. `; if (manaDrained > 0) { logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name} и исцеляется на ${healthGained} HP!`; } else if (damageDealtDrain > 0) { logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; } else { logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения, эффект не сработал!`; } if (drainTaunt !== "(Молчание)") logMsgDrain += ` (${drainTaunt})`; if (addToLogCallback) addToLogCallback(logMsgDrain, manaDrained > 0 || damageDealtDrain > 0 ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO); break; default: console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`); } // checkGameOverInternal(currentGameState, configToUse, localGameData); } /** * Логика принятия решения для AI (Баларда). * @param {Object} currentGameState - Текущее состояние игры. * @param {Object} passed_gameData - Данные игры (статы, абилки). * @param {Object} passed_GAME_CONFIG - Конфиг игры. * @param {Function} addToLogCallback - Для логгирования действий AI. * @returns {Object} - Объект с решением AI. */ function decideAiAction(currentGameState, passed_gameData, passed_GAME_CONFIG, addToLogCallback) { const configToUse = passed_GAME_CONFIG || GAME_CONFIG; const gameDataToUse = passed_gameData || require('./data'); const opponentState = currentGameState.opponent; const playerState = currentGameState.player; const isBalardFullySilencedByElena = opponentState.activeEffects.some( eff => eff.id === 'fullSilenceByElena' && eff.turnsLeft > 0 ); if (isBalardFullySilencedByElena) { if (addToLogCallback) addToLogCallback(`😵 ${opponentState.name} под действием "Гипнотического взгляда"! Атакует в смятении.`, configToUse.LOG_TYPE_EFFECT); return { actionType: 'attack' }; } const availableActions = []; const healAbility = gameDataToUse.opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_HEAL); if (healAbility && opponentState.currentResource >= healAbility.cost && healAbility.condition(opponentState, playerState, currentGameState, configToUse)) { availableActions.push({ weight: 80, type: 'ability', ability: healAbility }); } const silenceAbility = gameDataToUse.opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE); if (silenceAbility && opponentState.currentResource >= silenceAbility.cost && opponentState.silenceCooldownTurns <= 0 && // Check specific CD flag (!opponentState.abilityCooldowns || opponentState.abilityCooldowns[silenceAbility.id] === undefined || opponentState.abilityCooldowns[silenceAbility.id] <=0) && // Also check general CD if used silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) { const isPlayerLowHpForSilence = (playerState.currentHp / playerState.maxHp) * 100 < (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 40); const isPlayerAlreadySilenced = playerState.disabledAbilities.length > 0 || playerState.activeEffects.some(e => e.id.startsWith('playerSilencedOn_')); if (!isPlayerLowHpForSilence && !isPlayerAlreadySilenced) { availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility }); } } const drainAbility = gameDataToUse.opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN); if (drainAbility && opponentState.currentResource >= drainAbility.cost && opponentState.manaDrainCooldownTurns <= 0 && // Check specific CD flag (!opponentState.abilityCooldowns || opponentState.abilityCooldowns[drainAbility.id] === undefined || opponentState.abilityCooldowns[drainAbility.id] <=0) && // Also check general CD if used drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) { availableActions.push({ weight: 50, type: 'ability', ability: drainAbility }); } availableActions.push({ weight: 30, type: 'attack' }); if (availableActions.length > 0) { availableActions.sort((a, b) => b.weight - a.weight); const chosenAction = availableActions[0]; if (chosenAction.type === 'ability' && chosenAction.ability.id === configToUse.ABILITY_ID_BALARD_HEAL) { if (Math.random() < chosenAction.ability.successRate) { return { actionType: 'ability', ability: chosenAction.ability }; } else { if (addToLogCallback) addToLogCallback(`💨 ${opponentState.name} пытается использовать "${chosenAction.ability.name}", но терпит неудачу!`, configToUse.LOG_TYPE_INFO); const nextAction = availableActions.find(act => act.ability?.id !== configToUse.ABILITY_ID_BALARD_HEAL && act.type !== 'pass'); if (nextAction) return { actionType: nextAction.type, ability: nextAction.ability }; return { actionType: 'attack' }; } } return { actionType: chosenAction.type, ability: chosenAction.ability }; } return { actionType: 'pass', logMessage: { message: `${opponentState.name} не может совершить действие.`, type: configToUse.LOG_TYPE_INFO } }; } /** * Внутренняя функция для проверки конца игры без запуска полного цикла событий gameOver. * @param {Object} currentGameState * @param {Object} passed_GAME_CONFIG * @param {Object} passed_gameData - Не используется здесь, но для консистентности * @returns {boolean} true, если игра окончена. */ function checkGameOverInternal(currentGameState, passed_GAME_CONFIG, passed_gameData) { const configToUse = passed_GAME_CONFIG || GAME_CONFIG; // const gameDataToUse = passed_gameData || require('./data'); // Не используется здесь if (!currentGameState || currentGameState.isGameOver) return currentGameState ? currentGameState.isGameOver : true; const playerDead = currentGameState.player.currentHp <= 0; const opponentDead = currentGameState.opponent.currentHp <= 0; return playerDead || opponentDead; } module.exports = { processEffects, processDisabledAbilities, processPlayerAbilityCooldowns, updateBlockingStatus, getElenaTaunt, performAttack, applyAbilityEffect, decideAiAction, checkGameOverInternal };