// /server_modules/gameLogic.js const GAME_CONFIG = require('./config'); const gameData = require('./data'); // Загружаем один раз на уровне модуля // --- Вспомогательные Функции --- /** * Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода. * @param {Array} effectsArray - Массив активных эффектов бойца. * @param {Object} ownerState - Состояние бойца (currentHp, currentResource и т.д.). * @param {Object} ownerBaseStats - Базовые статы бойца (включая characterKey). * @param {String} ownerId - Технический ID слота бойца ('player' или 'opponent'). * @param {Object} currentGameState - Полное состояние игры. * @param {Function} addToLogCallback - Функция для добавления сообщений в лог игры. * @param {Object} configToUse - Конфигурационный объект игры. * @param {Object} gameDataForLogic - Полный объект gameData (для доступа к способностям и т.д.). * @returns {void} - Модифицирует effectsArray и ownerState напрямую. */ function processEffects(effectsArray, ownerState, ownerBaseStats, ownerId, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) { if (!effectsArray) return; const ownerName = ownerBaseStats.name; let effectsToRemoveIndexes = []; for (let i = 0; i < effectsArray.length; i++) { const eff = effectsArray[i]; const isNatureStrengthEffect = eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK; // if (isNatureStrengthEffect) { // Отладочный лог // console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect: ${eff.name}, justCast (before): ${eff.justCast}, turnsLeft (before): ${eff.turnsLeft}`); // } // --- Обработка эффектов с действием каждый ход (ДО уменьшения turnsLeft, если justCast === false) --- if (!eff.justCast) { // Эффекты, которые тикают, не должны тикать в ход наложения if (eff.isFullSilence && 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); } if ((eff.id === 'effect_' + configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || eff.id === 'effect_' + configToUse.ABILITY_ID_ALMAGEST_DEBUFF) && eff.power > 0) { const resourceToBurn = eff.power; if (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 { // Не уменьшаем turnsLeft для эффектов, которые должны длиться до следующей атаки // и не имеют фиксированного числа ходов (таких как Сила Природы, если бы она так работала). // В нашем случае Сила Природы имеет duration, поэтому turnsLeft уменьшается. eff.turnsLeft--; } // if (isNatureStrengthEffect) { // Отладочный лог // console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect: ${eff.name}, justCast (after): ${eff.justCast}, turnsLeft (after): ${eff.turnsLeft}`); // } // --- Удаление закончившихся эффектов --- if (eff.turnsLeft <= 0) { // if (isNatureStrengthEffect) { // Отладочный лог // console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect ${eff.name} REMOVED because turnsLeft is 0.`); // } effectsToRemoveIndexes.push(i); if (addToLogCallback) { addToLogCallback(`Эффект "${eff.name}" на ${ownerName} закончился.`, configToUse.LOG_TYPE_EFFECT); } } } for (let i = effectsToRemoveIndexes.length - 1; i >= 0; i--) { effectsArray.splice(effectsToRemoveIndexes[i], 1); } } /** Обрабатывает отсчет для отключенных (заглушенных) способностей игрока. */ function processDisabledAbilities(disabledAbilitiesArray, characterAbilities, characterName, addToLogCallback) { if (!disabledAbilitiesArray || disabledAbilitiesArray.length === 0) return; const stillDisabled = []; disabledAbilitiesArray.forEach(dis => { dis.turnsLeft--; if (dis.turnsLeft > 0) { stillDisabled.push(dis); } else { if (addToLogCallback) { const ability = characterAbilities.find(ab => ab.id === dis.abilityId); if (ability) addToLogCallback(`Способность ${characterName} "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO); } } }); disabledAbilitiesArray.length = 0; disabledAbilitiesArray.push(...stillDisabled); } /** Обрабатывает отсчет кулдаунов для способностей. */ function processPlayerAbilityCooldowns(cooldownsObject, abilitiesList, ownerName, addToLogCallback) { 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) { 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); } /** * Выбирает подходящую насмешку для Елены. */ function getElenaTaunt(trigger, context = {}, configToUse, gameDataForLogic = gameData, currentGameState) { if (!currentGameState || !currentGameState.player || currentGameState.player.characterKey !== 'elena') { return "(Молчание)"; } const opponentKey = currentGameState.opponent.characterKey; const tauntSystem = gameDataForLogic?.elenaTauntSystem; const tauntBranch = opponentKey === 'balard' ? tauntSystem?.aiBalard : tauntSystem?.pvpAlmagest; if (!tauntBranch) return "(Молчание)"; let potentialTaunts = []; const opponentHpPerc = (currentGameState.opponent.currentHp / currentGameState.opponent.maxHp) * 100; const isOpponentLowHpForDomination = opponentKey === 'balard' && opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT; const isOpponentNearDefeat = opponentHpPerc < 20; if (trigger === 'opponentNearDefeatCheck' && isOpponentNearDefeat && tauntBranch.onBattleState?.opponentNearDefeat) { potentialTaunts = tauntBranch.onBattleState.opponentNearDefeat; } else if (trigger === 'opponentAction' && context.abilityId) { const actionResponses = tauntBranch.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' && tauntBranch.onOpponentAction?.attackBlocked) { potentialTaunts = tauntBranch.onOpponentAction.attackBlocked; } else if (trigger === 'opponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) { potentialTaunts = tauntBranch.onOpponentAction.attackHits; } else if (trigger === 'playerActionCast' && context.abilityId && tauntBranch.onPlayerCast?.[context.abilityId]) { potentialTaunts = tauntBranch.onPlayerCast[context.abilityId]; } else if (trigger === 'playerBasicAttack') { if (isOpponentLowHpForDomination) { const pools = tauntBranch.base?.dominating || {}; potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ]; } else if (opponentKey === 'balard' && !isOpponentLowHpForDomination) { potentialTaunts = tauntBranch.base?.mercifulAttack || []; } else { potentialTaunts = tauntBranch.base?.generalAttack || tauntBranch.base?.mercifulAttack || []; } } else if (trigger === 'playerActionGeneral') { if (isOpponentLowHpForDomination) { const pools = tauntBranch.base?.dominating || {}; potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ]; } else if (opponentKey === 'balard' && !isOpponentLowHpForDomination) { potentialTaunts = tauntBranch.base?.mercifulCast || []; } else { potentialTaunts = tauntBranch.base?.generalCast || tauntBranch.base?.mercifulCast || []; } } else if (trigger === 'battleStart') { const startTaunts = (opponentKey === 'balard' ? tauntBranch.onBattleState?.startMerciful : tauntBranch.onBattleState?.start); if (startTaunts) potentialTaunts = startTaunts; } if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) { if (opponentKey === 'balard') { if (isOpponentLowHpForDomination) { const pools = tauntBranch.base?.dominating || {}; potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ]; } else { potentialTaunts = [...(tauntBranch.base?.mercifulAttack || []), ...(tauntBranch.base?.mercifulCast || [])]; } } else { potentialTaunts = [...(tauntBranch.base?.generalAttack || []), ...(tauntBranch.base?.generalCast || [])]; } } if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) return "(Молчание)"; return potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)] || "(Молчание)"; } // --- Основные Игровые Функции --- function performAttack(attackerState, defenderState, attackerBaseStats, defenderBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) { let damage = Math.floor(attackerBaseStats.attackPower * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)); let tauntMessagePart = ""; if (defenderState.isBlocking) { const initialDamage = damage; damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION); if (defenderState.characterKey === 'elena') { const blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState); if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`; } if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK); } else { let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`; if (defenderState.characterKey === 'elena') { const hitTaunt = getElenaTaunt('opponentAttackHit', {}, configToUse, gameDataForLogic, currentGameState); if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`; } if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE); } defenderState.currentHp = Math.max(0, defenderState.currentHp - damage); } function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) { let tauntMessagePart = ""; 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) { const initialDamage = damage; damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION); if (targetState.characterKey === 'elena') { const blockTaunt = getElenaTaunt('opponentAttackBlocked', {abilityId: ability.id} , configToUse, gameDataForLogic, currentGameState); if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`; } if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}"! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK); } targetState.currentHp = Math.max(0, targetState.currentHp - damage); if (addToLogCallback && !targetState.isBlocking) { let hitMessage = `💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`; if (targetState.characterKey === 'elena') { const hitTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState); if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`; } addToLogCallback(hitMessage, 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') { const opponentRole = casterState.id === configToUse.PLAYER_ID ? configToUse.OPPONENT_ID : configToUse.PLAYER_ID; const opponentCurrentState = currentGameState[opponentRole]; const opponentDataForDesc = opponentCurrentState ? gameDataForLogic[opponentCurrentState.characterKey + 'BaseStats'] : gameDataForLogic.playerBaseStats; // Фоллбэк effectDescription = ability.descriptionFunction(configToUse, opponentDataForDesc); } 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: tauntMessagePart = ""; // Сбрасываем перед каждым дизейблом if (targetState.characterKey === 'elena') { const disableTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState); if (disableTaunt !== "(Молчание)") tauntMessagePart = ` (${disableTaunt})`; } if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE && casterState.characterKey === 'elena') { 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 && casterState.characterKey === 'balard') { const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE; const silenceOutcome = success ? 'success' : 'fail'; const specificSilenceTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState); tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : ""; if (success) { const targetAbilities = gameDataForLogic.playerAbilities; const availableAbilities = targetAbilities.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}" Елены заблокировано!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT); } else { if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO); } } else { if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO); } } else if (ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE && casterState.characterKey === 'almagest') { const effectId = 'fullSilenceByAlmagest'; 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} хода, и он(а) получает урон!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT); } } break; case configToUse.ACTION_TYPE_DEBUFF: tauntMessagePart = ""; if (targetState.characterKey === 'elena') { const debuffTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState); if (debuffTaunt !== "(Молчание)") tauntMessagePart = ` (${debuffTaunt})`; } if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF) { 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, }); if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT); } } break; case configToUse.ACTION_TYPE_DRAIN: if (casterState.characterKey === 'balard') { let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0; tauntMessagePart = ""; if (targetState.characterKey === 'elena') { const drainTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id }, configToUse, gameDataForLogic, currentGameState); if (drainTaunt !== "(Молчание)") tauntMessagePart = ` (${drainTaunt})`; } if (ability.powerDamage > 0) { let baseDamageDrain = ability.powerDamage; if (targetState.isBlocking) { baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION); let blockDrainTaunt = ""; if (targetState.characterKey === 'elena') { blockDrainTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState); if (blockDrainTaunt !== "(Молчание)") blockDrainTaunt = ` (${blockDrainTaunt})`; } 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; } 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} для похищения, эффект не сработал!`; } logMsgDrain += tauntMessagePart; 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}"`); } } function decideAiAction(currentGameState, gameDataForLogic = gameData, configToUse, addToLogCallback) { const opponentState = currentGameState.opponent; const playerState = currentGameState.player; if (opponentState.characterKey !== 'balard') { console.warn("[AI DEBUG] decideAiAction called for non-Balard opponent. This should not happen."); return { actionType: 'pass', logMessage: { message: `${opponentState.name} (не AI) пропускает ход.`, type: configToUse.LOG_TYPE_INFO } }; } 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 opponentAbilities = gameDataForLogic.opponentAbilities; const healAbility = 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, requiresSuccessCheck: true, successRate: healAbility.successRate }); } const silenceAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE); if (silenceAbility && opponentState.currentResource >= silenceAbility.cost && (opponentState.silenceCooldownTurns === undefined || opponentState.silenceCooldownTurns <= 0) && (!opponentState.abilityCooldowns || opponentState.abilityCooldowns[silenceAbility.id] === undefined || opponentState.abilityCooldowns[silenceAbility.id] <=0) && silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) { const playerHpPercent = (playerState.currentHp / playerState.maxHp) * 100; if (playerHpPercent > (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 40)) { availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility, requiresSuccessCheck: true, successRate: configToUse.SILENCE_SUCCESS_RATE }); } } const drainAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN); if (drainAbility && opponentState.currentResource >= drainAbility.cost && (opponentState.manaDrainCooldownTurns === undefined || opponentState.manaDrainCooldownTurns <= 0) && (!opponentState.abilityCooldowns || opponentState.abilityCooldowns[drainAbility.id] === undefined || opponentState.abilityCooldowns[drainAbility.id] <=0) && drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) { availableActions.push({ weight: 50, type: 'ability', ability: drainAbility }); } availableActions.push({ weight: 30, type: 'attack' }); if (availableActions.length === 0) { return { actionType: 'pass', logMessage: { message: `${opponentState.name} не может совершить действие.`, type: configToUse.LOG_TYPE_INFO } }; } availableActions.sort((a, b) => b.weight - a.weight); for (const action of availableActions) { if (action.requiresSuccessCheck) { if (Math.random() < action.successRate) { return { actionType: action.type, ability: action.ability }; } else { if (addToLogCallback) addToLogCallback(`💨 ${opponentState.name} пытается использовать "${action.ability.name}", но терпит неудачу!`, configToUse.LOG_TYPE_INFO); continue; } } else { return { actionType: action.type, ability: action.ability }; } } return { actionType: 'attack' }; } function checkGameOverInternal(currentGameState, configToUse, gameDataForLogic = gameData) { 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 };