// /server/game/logic/aiLogic.js // GAME_CONFIG и gameData (или dataUtils) будут передаваться в decideAiAction как параметры, // но для удобства можно импортировать GAME_CONFIG здесь, если он нужен для внутренних констант AI, // не зависящих от передаваемого конфига. // const GAME_CONFIG_STATIC = require('../../core/config'); // Если нужно для чего-то внутреннего /** * Логика принятия решения для AI (Балард). * @param {object} currentGameState - Текущее состояние игры. * @param {object} dataUtils - Утилиты для доступа к данным игры (getCharacterData, getCharacterAbilities и т.д.). * @param {object} configToUse - Конфигурационный объект игры (переданный GAME_CONFIG). * @param {function} addToLogCallback - Функция для добавления лога (опционально, если AI должен логировать свои "мысли"). * @returns {object} Объект с действием AI ({ actionType: 'attack' | 'ability' | 'pass', ability?: object, logMessage?: {message, type} }). */ function decideAiAction(currentGameState, dataUtils, configToUse, addToLogCallback) { const opponentState = currentGameState.opponent; // AI Балард всегда в слоте opponent const playerState = currentGameState.player; // Игрок всегда в слоте player (в AI режиме) // Убеждаемся, что это AI Балард и есть необходимые данные if (opponentState.characterKey !== 'balard' || !dataUtils) { console.warn("[AI Logic] decideAiAction called for non-Balard opponent or missing dataUtils. Passing turn."); if (addToLogCallback) addToLogCallback(`${opponentState.name || 'AI'} пропускает ход из-за внутренней ошибки.`, configToUse.LOG_TYPE_SYSTEM); return { actionType: 'pass', logMessage: { message: `${opponentState.name || 'AI'} пропускает ход.`, type: configToUse.LOG_TYPE_INFO } }; } const balardCharacterData = dataUtils.getCharacterData('balard'); if (!balardCharacterData || !balardCharacterData.abilities) { console.warn("[AI Logic] Failed to get Balard's character data or abilities. Passing turn."); if (addToLogCallback) addToLogCallback(`AI Балард пропускает ход из-за ошибки загрузки данных.`, configToUse.LOG_TYPE_SYSTEM); return { actionType: 'pass', logMessage: { message: `Балард пропускает ход.`, type: configToUse.LOG_TYPE_INFO } }; } const balardAbilities = balardCharacterData.abilities; // Проверка полного безмолвия Баларда (от Гипнотического Взгляда Елены и т.п.) const isBalardFullySilenced = opponentState.activeEffects.some( eff => eff.isFullSilence && eff.turnsLeft > 0 ); if (isBalardFullySilenced) { // AI под полным безмолвием просто атакует. // Лог о безмолвии добавляется в GameInstance перед вызовом этой функции или при обработке атаки. // Здесь можно добавить лог о "вынужденной" атаке, если нужно. if (addToLogCallback) { // Проверяем, не был ли лог о безмолвии уже добавлен в этом ходу (чтобы не спамить) // Это упрощенная проверка, в реальном приложении можно использовать флаги или более сложную логику. // if (!currentGameState.logContainsThisTurn || !currentGameState.logContainsThisTurn.includes('под действием Безмолвия')) { // addToLogCallback(`😵 ${opponentState.name} под действием Безмолвия! Атакует в смятении.`, configToUse.LOG_TYPE_EFFECT); // if(currentGameState) currentGameState.logContainsThisTurn = (currentGameState.logContainsThisTurn || "") + 'под действием Безмолвия'; // } } return { actionType: 'attack' }; } const availableActions = []; // 1. Проверяем способность "Покровительство Тьмы" (Лечение) const healAbility = balardAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_HEAL); if (healAbility && opponentState.currentResource >= healAbility.cost && (opponentState.abilityCooldowns?.[healAbility.id] || 0) <= 0 && // Общий КД healAbility.condition(opponentState, playerState, currentGameState, configToUse)) { availableActions.push({ weight: 80, type: 'ability', ability: healAbility, requiresSuccessCheck: true, successRate: healAbility.successRate }); } // 2. Проверяем способность "Эхо Безмолвия" const silenceAbility = balardAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE); if (silenceAbility && opponentState.currentResource >= silenceAbility.cost && (opponentState.silenceCooldownTurns === undefined || opponentState.silenceCooldownTurns <= 0) && // Спец. КД (opponentState.abilityCooldowns?.[silenceAbility.id] || 0) <= 0 && // Общий КД silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) { // Условие в silenceAbility.condition уже проверяет, что Елена не под безмолвием availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility, requiresSuccessCheck: true, successRate: configToUse.SILENCE_SUCCESS_RATE }); } // 3. Проверяем способность "Похищение Света" (Вытягивание маны и урон) const drainAbility = balardAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN); if (drainAbility && opponentState.currentResource >= drainAbility.cost && (opponentState.manaDrainCooldownTurns === undefined || opponentState.manaDrainCooldownTurns <= 0) && // Спец. КД (opponentState.abilityCooldowns?.[drainAbility.id] || 0) <= 0 && // Общий КД drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) { availableActions.push({ weight: 50, type: 'ability', ability: drainAbility }); } // 4. Базовая атака - всегда доступна как запасной вариант с низким весом availableActions.push({ weight: 30, type: 'attack' }); if (availableActions.length === 0) { // Этого не должно происходить, так как атака всегда добавляется if (addToLogCallback) addToLogCallback(`${opponentState.name} не может совершить действие (нет доступных).`, configToUse.LOG_TYPE_INFO); return { actionType: 'pass', logMessage: { message: `${opponentState.name} пропускает ход.`, type: configToUse.LOG_TYPE_INFO } }; } // Сортируем действия по весу в порядке убывания (самые приоритетные в начале) availableActions.sort((a, b) => b.weight - a.weight); // console.log(`[AI Logic] Available actions for Balard, sorted by weight:`, JSON.stringify(availableActions.map(a => ({type: a.type, name: a.ability?.name, weight: a.weight})), null, 2)); // Перебираем действия в порядке приоритета и выбираем первое подходящее for (const action of availableActions) { if (action.type === 'ability') { if (action.requiresSuccessCheck) { // Для способностей с шансом успеха, "бросаем кубик" if (Math.random() < action.successRate) { if (addToLogCallback) addToLogCallback(`⭐ ${opponentState.name} решает использовать "${action.ability.name}" (попытка успешна)...`, configToUse.LOG_TYPE_INFO); return { actionType: action.type, ability: action.ability }; } else { // Провал шанса, добавляем лог и ИИ переходит к следующему действию в списке (если есть) if (addToLogCallback) addToLogCallback(`💨 ${opponentState.name} пытался использовать "${action.ability.name}", но шанс не сработал!`, configToUse.LOG_TYPE_INFO); continue; // Пробуем следующее приоритетное действие } } else { // Способность без проверки шанса (например, Похищение Света) if (addToLogCallback) addToLogCallback(`⭐ ${opponentState.name} решает использовать "${action.ability.name}"...`, configToUse.LOG_TYPE_INFO); return { actionType: action.type, ability: action.ability }; } } else if (action.type === 'attack') { // Атака - если дошли до нее, значит, более приоритетные способности не были выбраны или провалили шанс if (addToLogCallback) addToLogCallback(`🦶 ${opponentState.name} решает атаковать...`, configToUse.LOG_TYPE_INFO); return { actionType: 'attack' }; } } // Фоллбэк, если по какой-то причине ни одно действие не было выбрано (не должно происходить, если атака всегда есть) console.warn("[AI Logic] AI Balard failed to select any action after iterating. Defaulting to pass."); if (addToLogCallback) addToLogCallback(`${opponentState.name} не смог выбрать подходящее действие. Пропускает ход.`, configToUse.LOG_TYPE_INFO); return { actionType: 'pass', logMessage: { message: `${opponentState.name} пропускает ход.`, type: configToUse.LOG_TYPE_INFO } }; } module.exports = { decideAiAction };