727 lines
58 KiB
JavaScript
727 lines
58 KiB
JavaScript
// /server_modules/gameLogic.js
|
||
const GAME_CONFIG = require('./config');
|
||
const gameData = require('./data'); // Загружаем один раз на уровне модуля
|
||
|
||
// --- Вспомогательные Функции для gameLogic ---
|
||
|
||
// Вспомогательная функция для получения данных персонажа (baseStats и abilities)
|
||
// Нужна здесь, так как объект gameData сам по себе не имеет этих методов.
|
||
// Принимает gameDataForLogic как аргумент для гибкости, по умолчанию использует глобальный gameData.
|
||
function _getCharacterDataForLogic(key, gameDataForLogic = gameData) {
|
||
if (!key) return null;
|
||
switch (key) {
|
||
case 'elena': return { baseStats: gameDataForLogic.playerBaseStats, abilities: gameDataForLogic.playerAbilities };
|
||
case 'balard': return { baseStats: gameDataForLogic.opponentBaseStats, abilities: gameDataForLogic.opponentAbilities }; // Балард использует opponentAbilities
|
||
case 'almagest': return { baseStats: gameDataForLogic.almagestBaseStats, abilities: gameDataForLogic.almagestAbilities }; // Альмагест использует almagestAbilities
|
||
default: console.error(`_getCharacterDataForLogic: Неизвестный ключ персонажа "${key}"`); return null;
|
||
}
|
||
}
|
||
// Вспомогательная функция для получения только базовых статов персонажа
|
||
function _getCharacterBaseDataForLogic(key, gameDataForLogic = gameData) {
|
||
const charData = _getCharacterDataForLogic(key, gameDataForLogic);
|
||
return charData ? charData.baseStats : null;
|
||
}
|
||
// Вспомогательная функция для получения только способностей персонажа
|
||
function _getCharacterAbilitiesForLogic(key, gameDataForLogic = gameData) {
|
||
const charData = _getCharacterDataForLogic(key, gameDataForLogic);
|
||
return charData ? charData.abilities : null;
|
||
}
|
||
|
||
|
||
/**
|
||
* Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода.
|
||
* Длительность эффекта уменьшается на 1.
|
||
* Периодические эффекты (DoT, ресурсный дебафф и т.п.) срабатывают, если эффект не "justCast" в этом ходу.
|
||
* @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];
|
||
|
||
// --- Применяем эффект (DoT, сжигание ресурса и т.п.), если он не только что наложен в этом ходу ---
|
||
if (!eff.justCast) {
|
||
// Обработка урона от эффектов полного безмолвия (Гипнотический Взгляд, Раскол Разума)
|
||
// Эти эффекты наносят урон цели В КОНЦЕ ее хода
|
||
if (eff.isFullSilence && typeof eff.power === 'number' && eff.power > 0) {
|
||
const damage = eff.power;
|
||
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||
ownerState.currentHp = Math.max(0, Math.round(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, Math.round(ownerState.currentResource - actualBurn));
|
||
if (addToLogCallback) addToLogCallback(`🔥 Эффект "${eff.name}" сжигает ${actualBurn} ${ownerBaseStats.resourceName} у ${ownerName}!`, configToUse.LOG_TYPE_EFFECT);
|
||
}
|
||
}
|
||
// Примечание: Отложенные эффекты (например, Сила Природы) применяют свою силу в gameInstance.processPlayerAction (после атаки), а не здесь.
|
||
}
|
||
|
||
// --- Уменьшаем длительность ---
|
||
eff.turnsLeft--;
|
||
eff.justCast = false; // Эффект больше не считается "just cast" после обработки этого хода
|
||
|
||
// --- Отмечаем для удаления, если длительность закончилась ---
|
||
if (eff.turnsLeft <= 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);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Обрабатывает отсчет для отключенных (заглушенных) способностей игрока в конце его хода.
|
||
* Длительность заглушения уменьшается на 1.
|
||
* @param {Array<object>} disabledAbilitiesArray - Массив объектов заглушенных способностей.
|
||
* @param {Array<object>} characterAbilities - Полный список способностей персонажа (для получения имени).
|
||
* @param {string} characterName - Имя персонажа (для лога).
|
||
* @param {function} addToLogCallback - Функция для добавления лога.
|
||
* @returns {void} - Модифицирует disabledAbilitiesArray напрямую.
|
||
*/
|
||
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);
|
||
// Проверка на заглушающий эффект тоже должна быть удалена из activeEffects в processEffects
|
||
// Здесь мы только обрабатываем список disabledAbilities, удаляя запись
|
||
if (ability) addToLogCallback(`Способность ${characterName} "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
});
|
||
// Очищаем исходный массив и добавляем только те, что еще активны
|
||
disabledAbilitiesArray.length = 0;
|
||
disabledAbilitiesArray.push(...stillDisabled);
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает отсчет общих кулдаунов для способностей в конце хода.
|
||
* Длительность кулдауна уменьшается на 1.
|
||
* @param {object} cooldownsObject - Объект с кулдаунами способностей ({ abilityId: turnsLeft }).
|
||
* @param {Array<object>} abilitiesList - Полный список способностей персонажа (для получения имени).
|
||
* @param {string} ownerName - Имя персонажа (для лога).
|
||
* @param {function} addToLogCallback - Функция для добавления лога.
|
||
* @returns {void} - Модифицирует cooldownsObject напрямую.
|
||
*/
|
||
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' на основе активных эффектов.
|
||
* @param {object} fighterState - Состояние бойца.
|
||
* @returns {void} - Модифицирует fighterState.isBlocking.
|
||
*/
|
||
function updateBlockingStatus(fighterState) {
|
||
if (!fighterState) return;
|
||
// Боец считается блокирующим, если у него есть активный эффект, дающий блок (grantsBlock: true) с turnsLeft > 0
|
||
fighterState.isBlocking = fighterState.activeEffects.some(eff => eff.grantsBlock && eff.turnsLeft > 0);
|
||
}
|
||
|
||
|
||
/**
|
||
* Получает случайную насмешку из системы насмешек для определенного персонажа.
|
||
* Ищет фразу в gameData.tauntSystem[speakerCharacterKey][opponentCharacterKey][trigger][context].
|
||
* @param {string} speakerCharacterKey - Ключ персонажа, который произносит насмешку ('elena' или 'almagest' или 'balard').
|
||
* @param {string} trigger - Тип события, вызвавшего насмешку (например, 'selfCastAbility', 'onOpponentAction', 'battleStart', 'basicAttack', 'opponentNearDefeatCheck').
|
||
* @param {object} context - Дополнительный контекст (например, { abilityId: 'fireball' }, { outcome: 'success' }).
|
||
* @param {object} configToUse - Конфигурационный объект игры.
|
||
* @param {object} gameDataForLogic - Полный объект gameData.
|
||
* @param {object} currentGameState - Текущее состояние игры.
|
||
* @returns {string} Текст насмешки или "(Молчание)".
|
||
*/
|
||
function getRandomTaunt(speakerCharacterKey, trigger, context = {}, configToUse, gameDataForLogic = gameData, currentGameState) {
|
||
// Проверяем наличие системы насмешек для говорящего персонажа
|
||
const speakerTauntSystem = gameDataForLogic?.tauntSystem?.[speakerCharacterKey];
|
||
if (!speakerTauntSystem) return "(Молчание)"; // Нет насмешек для этого персонажа
|
||
|
||
// Определяем противника, чтобы выбрать соответствующую ветку насмешек
|
||
// Для этого нужно найти в gameState, кто из player/opponent имеет characterKey говорящего,
|
||
// и взять characterKey другого.
|
||
const speakerRole = currentGameState?.player?.characterKey === speakerCharacterKey ?
|
||
GAME_CONFIG.PLAYER_ID :
|
||
(currentGameState?.opponent?.characterKey === speakerCharacterKey ?
|
||
GAME_CONFIG.OPPONENT_ID : null);
|
||
|
||
if (speakerRole === null) {
|
||
console.warn(`getRandomTaunt: Speaker character key "${speakerCharacterKey}" not found in current game state roles.`);
|
||
return "(Молчание)";
|
||
}
|
||
|
||
const opponentRole = speakerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||
const opponentCharacterKey = currentGameState?.[opponentRole]?.characterKey;
|
||
|
||
const tauntBranch = speakerTauntSystem[opponentCharacterKey];
|
||
if (!tauntBranch) {
|
||
// console.warn(`getRandomTaunt: No taunt branch found for speaker "${speakerCharacterKey}" against opponent "${opponentCharacterKey}".`);
|
||
return "(Молчание)"; // Нет насмешек против этого оппонента
|
||
}
|
||
|
||
|
||
let potentialTaunts = [];
|
||
|
||
// Навигация по структуре tauntSystem в зависимости от триггера и контекста
|
||
if (trigger === 'battleStart') {
|
||
potentialTaunts = tauntBranch.onBattleState?.start;
|
||
}
|
||
else if (trigger === 'opponentNearDefeatCheck') { // Проверка на низкое HP противника для специальных фраз
|
||
const opponentState = currentGameState?.[opponentRole]; // Состояние противника для проверки HP
|
||
// Проверяем, что состояние оппонента существует и его HP ниже порога (например, 20%)
|
||
if (opponentState && opponentState.maxHp > 0 && opponentState.currentHp / opponentState.maxHp < 0.20) {
|
||
potentialTaunts = tauntBranch.onBattleState?.opponentNearDefeat;
|
||
}
|
||
}
|
||
else if (trigger === 'selfCastAbility' && context.abilityId) {
|
||
potentialTaunts = tauntBranch.selfCastAbility?.[context.abilityId];
|
||
}
|
||
else if (trigger === 'basicAttack' && tauntBranch.basicAttack) {
|
||
const opponentState = currentGameState?.[opponentRole]; // Состояние противника
|
||
// Специальная логика для базовой атаки Елены против Баларда (милосердие/доминирование)
|
||
if (speakerCharacterKey === 'elena' && opponentCharacterKey === 'balard' && opponentState) {
|
||
const opponentHpPerc = (opponentState.currentHp / opponentState.maxHp) * 100;
|
||
if (opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT) {
|
||
potentialTaunts = tauntBranch.basicAttack.dominating;
|
||
} else {
|
||
potentialTaunts = tauntBranch.basicAttack.merciful;
|
||
}
|
||
} else { // Общая логика для PvP или Елена/Балард вне порога
|
||
potentialTaunts = tauntBranch.basicAttack.general;
|
||
}
|
||
}
|
||
// Реакция на действие противника
|
||
else if (trigger === 'onOpponentAction' && 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]; // Например, onOpponentAction.silence.success
|
||
} else if (Array.isArray(actionResponses)) {
|
||
potentialTaunts = actionResponses; // Прямой массив фраз для способности
|
||
}
|
||
}
|
||
}
|
||
// Реакция на попадание/блок атаки противника
|
||
// Примечание: Эти триггеры срабатывают, когда по ГОВОРЯЩЕМУ попала атака или он ее заблокировал.
|
||
// Вызываются из performAttack, где известно, кто атакует и кто защищается.
|
||
else if (trigger === 'onOpponentAttackBlocked' && tauntBranch.onOpponentAction?.attackBlocked) {
|
||
potentialTaunts = tauntBranch.onOpponentAction.attackBlocked;
|
||
}
|
||
else if (trigger === 'onOpponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) {
|
||
potentialTaunts = tauntBranch.onOpponentAction.attackHits;
|
||
}
|
||
|
||
// Если по прямому триггеру не найдено, возвращаем "(Молчание)".
|
||
// Можно добавить фоллбэк на общие фразы, если требуется более разговорчивый персонаж.
|
||
// Например: if ((!potentialTaunts || potentialTaunts.length === 0) && tauntBranch.basicAttack?.general) { potentialTaunts = tauntBranch.basicAttack.general; }
|
||
|
||
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
|
||
return "(Молчание)"; // Возвращаем молчание, если ничего не найдено
|
||
}
|
||
|
||
// Возвращаем случайную фразу из найденного массива
|
||
const selectedTaunt = potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)];
|
||
return selectedTaunt || "(Молчание)"; // Фоллбэк на "(Молчание)" если массив был пуст после всех проверок
|
||
}
|
||
|
||
|
||
// --- Основные Игровые Функции ---
|
||
|
||
/**
|
||
* Обрабатывает базовую атаку одного бойца по другому.
|
||
* @param {object} attackerState - Состояние атакующего бойца.
|
||
* @param {object} defenderState - Состояние защищающегося бойца.
|
||
* @param {object} attackerBaseStats - Базовые статы атакующего.
|
||
* @param {object} defenderBaseStats - Базовые статы защищающегося.
|
||
* @param {object} currentGameState - Текущее состояние игры (для насмешек).
|
||
* @param {function} addToLogCallback - Функция для добавления лога.
|
||
* @param {object} configToUse - Конфигурация игры.
|
||
* @param {object} gameDataForLogic - Данные игры (для насмешек).
|
||
*/
|
||
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' || defenderState.characterKey === 'almagest') {
|
||
// getRandomTaunt принимает speaker (защищающийся), trigger, context, config, gameData, gameState
|
||
const blockTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
|
||
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
|
||
}
|
||
|
||
if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
||
} else {
|
||
let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
|
||
// Проверка на насмешку ОТ защищающегося (Елены или Альмагест) при попадании атаки
|
||
if (defenderState.characterKey === 'elena' || defenderState.characterKey === 'almagest') {
|
||
// getRandomTaunt принимает speaker (защищающийся), trigger, context, config, gameData, gameState
|
||
const hitTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackHit', {}, configToUse, gameDataForLogic, currentGameState);
|
||
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
|
||
}
|
||
if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
|
||
}
|
||
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
||
}
|
||
|
||
/**
|
||
* Применяет эффект способности.
|
||
* @param {object} ability - Объект способности.
|
||
* @param {object} casterState - Состояние бойца, применившего способность.
|
||
* @param {object} targetState - Состояние цели способности.
|
||
* @param {object} casterBaseStats - Базовые статы кастера.
|
||
* @param {object} targetBaseStats - Базовые статы цели.
|
||
* @param {object} currentGameState - Текущее состояние игры (для насмешек).
|
||
* @param {function} addToLogCallback - Функция для добавления лога.
|
||
* @param {object} configToUse - Конфигурация игры.
|
||
* @param {object} gameDataForLogic - Данные игры (для насмешек).
|
||
*/
|
||
function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
|
||
let tauntMessagePart = ""; // Переменная для насмешки, если она связана с результатом эффекта или реакцией цели
|
||
|
||
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под способность противника
|
||
if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||
// Триггер 'onOpponentAction' с abilityId противника
|
||
const reactionTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id }, configToUse, gameDataForLogic, currentGameState);
|
||
if (reactionTaunt !== "(Молчание)") tauntMessagePart = ` (${reactionTaunt})`;
|
||
} else {
|
||
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) {
|
||
// ИСПРАВЛЕНО: Округляем результат прибавления HP
|
||
casterState.currentHp = Math.round(casterState.currentHp + actualHeal);
|
||
// --- ИЗМЕНЕНИЕ: Добавляем название способности в лог лечения ---
|
||
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!${tauntMessagePart}`, configToUse.LOG_TYPE_HEAL);
|
||
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.${tauntMessagePart}`, 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' || targetState.characterKey === 'almagest') {
|
||
// const blockTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAttackBlocked', {abilityId: ability.id} , configToUse, gameDataForLogic, currentGameState);
|
||
// if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
|
||
// }
|
||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
||
}
|
||
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
||
if (addToLogCallback && !targetState.isBlocking) {
|
||
let hitMessage = `💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!${tauntMessagePart}`;
|
||
// Проверка на насмешку ОТ цели (Елены или Альмагест), если по ней попала способность - перенесено наверх
|
||
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||
// const hitTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
||
// if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
|
||
// }
|
||
addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_BUFF:
|
||
// Если бафф уже активен, не применяем его повторно (эта проверка уже есть в gameInstance)
|
||
// Проверка на .some здесь опциональна, т.к. вызывающий код должен гарантировать уникальность
|
||
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?.characterKey ? _getCharacterBaseDataForLogic(opponentCurrentState.characterKey, gameDataForLogic) : null; // ИСПОЛЬЗУЕМ _getCharacterBaseDataForLogic
|
||
effectDescription = ability.descriptionFunction(configToUse, opponentDataForDesc);
|
||
}
|
||
// isDelayed: true используется для эффектов, которые срабатывают ПОСЛЕ следующего действия (например, Сила Природы).
|
||
// duration: исходная длительность из данных, turnsLeft: сколько ходов осталось (включая текущий, если !justCast)
|
||
casterState.activeEffects.push({
|
||
id: ability.id, name: ability.name, description: effectDescription,
|
||
type: ability.type, duration: ability.duration, // Сохраняем исходную длительность для отображения в UI или логики
|
||
turnsLeft: ability.duration, // Длительность жизни эффекта в ходах владельца
|
||
grantsBlock: !!ability.grantsBlock,
|
||
isDelayed: !!ability.isDelayed, // Флаг, что эффект отложенный (срабатывает после действия)
|
||
justCast: true // Флаг, что эффект только что наложен (для логики processEffects)
|
||
});
|
||
if (ability.grantsBlock) updateBlockingStatus(casterState); // Обновляем статус блока кастера, если бафф его дает
|
||
// Насмешки при применении баффа (selfCastAbility) добавляются в GameInstance перед вызовом applyAbilityEffect
|
||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||
} else {
|
||
// Сообщение "уже активен" отправляется из gameInstance перед вызовом applyAbilityEffect
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DISABLE: // Безмолвие, Стан и т.п.
|
||
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под дизейбл противника - перенесено наверх
|
||
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||
// const disableTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
||
// if (disableTaunt !== "(Молчание)") tauntMessagePart = ` (${disableTaunt})`;
|
||
// }
|
||
|
||
// Гипнотический взгляд Елены / Раскол Разума Альмагест (полное безмолвие)
|
||
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
|
||
const effectId = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
|
||
// Не накладываем повторно, если эффект уже есть на цели
|
||
if (!targetState.activeEffects.some(e => e.id === effectId)) {
|
||
targetState.activeEffects.push({
|
||
id: effectId, name: ability.name, description: ability.description,
|
||
type: ability.type, duration: ability.effectDuration, turnsLeft: ability.effectDuration, // Длительность в ходах цели
|
||
power: ability.power, // Урон от эффекта (применяется в конце хода цели)
|
||
isFullSilence: true,
|
||
justCast: true // Эффект только что наложен
|
||
});
|
||
if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}! Способности ${targetBaseStats.name} заблокированы на ${ability.effectDuration} хода, и он(а) получает урон!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
// Эхо Безмолвия Баларда (заглушает случайную абилку)
|
||
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';
|
||
// Реакция цели (Елены) на успех/провал безмолвия Баларда - перенесено наверх, но с context.outcome
|
||
// if (targetState.characterKey === 'elena') { // Балард применяет это только на Елену
|
||
// const specificSilenceTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
|
||
// tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : "";
|
||
// } else {
|
||
// tauntMessagePart = ""; // Другие персонажи не реагируют на Безмолвие Баларда
|
||
// }
|
||
|
||
// Нужно получить насмешку с outcome здесь, так как она зависит от результата броска шанса
|
||
// Временно сохраняем общую насмешку и получаем специфичную
|
||
let specificSilenceTaunt = "(Молчание)";
|
||
if (targetState.characterKey === 'elena') { // Балард применяет это только на Елену
|
||
specificSilenceTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
|
||
}
|
||
tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : tauntMessagePart; // Используем специфичную, если найдена, иначе общую (хотя общая для этого абилки вряд ли есть)
|
||
|
||
|
||
if (success) {
|
||
const targetAbilities = _getCharacterAbilitiesForLogic(targetState.characterKey, gameDataForLogic); // Глушим абилки цели
|
||
// Фильтруем способности, которые еще не заглушены этим типом безмолвия
|
||
const availableAbilities = targetAbilities.filter(pa =>
|
||
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
|
||
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`) // Проверка на эффект заглушения для UI/ProcessEffects
|
||
);
|
||
|
||
if (availableAbilities.length > 0) {
|
||
const abilityToSilence = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
|
||
const turns = configToUse.SILENCE_DURATION; // Длительность из конфига (в ходах цели)
|
||
// Добавляем запись о заглушенной способности в disabledAbilities цели
|
||
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); // +1, т.к. длительность уменьшается в конце хода цели
|
||
// Добавляем эффект заглушения в activeEffects цели (для UI и ProcessEffects)
|
||
const silenceEffectIdOnPlayer = `playerSilencedOn_${abilityToSilence.id}`;
|
||
targetState.activeEffects.push({
|
||
id: silenceEffectIdOnPlayer, name: `Безмолвие: ${abilityToSilence.name}`,
|
||
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
|
||
type: configToUse.ACTION_TYPE_DISABLE, sourceAbilityId: ability.id, // Добавлено sourceAbilityId
|
||
duration: turns, turnsLeft: turns + 1,
|
||
justCast: true // Эффект только что наложен
|
||
});
|
||
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" ${targetBaseStats.name} заблокировано на ${turns} хода!${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);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DEBUFF: // Ослабления, DoT и т.п.
|
||
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под дебафф противника - перенесено наверх
|
||
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||
// const debuffTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {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; // Уникальный 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,
|
||
duration: ability.effectDuration, turnsLeft: ability.effectDuration, // Длительность в ходах цели
|
||
power: ability.power, // Количество сжигаемого ресурса в ход (применяется в конце хода цели)
|
||
justCast: true // Эффект только что наложен
|
||
});
|
||
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DRAIN: // Похищение Света Баларда (наносит урон, вытягивает ресурс, лечит кастера)
|
||
if (casterState.characterKey === 'balard') { // Это способность только Баларда
|
||
let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0;
|
||
// tauntMessagePart уже получена в начале функции
|
||
|
||
// Сначала урон от способности
|
||
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' || targetState.characterKey === 'almagest') {
|
||
blockDrainTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
|
||
if (blockDrainTaunt !== "(Молчание)") blockDrainTaunt = ` (${blockDrainTaunt})`;
|
||
}
|
||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует часть урона от "${ability.name}" от ${casterBaseStats.name}! Урон снижен до ${baseDamageDrain}.${blockDrainTaunt}`, configToUse.LOG_TYPE_BLOCK);
|
||
}
|
||
damageDealtDrain = Math.max(0, baseDamageDrain);
|
||
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damageDealtDrain));
|
||
}
|
||
|
||
// Затем вытягивание ресурса и лечение кастера
|
||
const potentialDrain = ability.powerManaDrain;
|
||
const actualDrain = Math.min(potentialDrain, targetState.currentResource);
|
||
|
||
if (actualDrain > 0) {
|
||
// ИСПРАВЛЕНО: Округляем результат вычитания ресурса
|
||
targetState.currentResource = Math.max(0, Math.round(targetState.currentResource - actualDrain));
|
||
manaDrained = actualDrain;
|
||
const potentialHeal = Math.floor(manaDrained * ability.powerHealthGainFactor);
|
||
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
|
||
// ИСПРАВЛЕНО: Округляем результат прибавления HP
|
||
casterState.currentHp = Math.round(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 { // Ни урона, ни вытягивания ресурса
|
||
// ИСПРАВЛЕНО: targetBaseStats.resourceName -> targetState.resourceName (или defenderBaseStats.resourceName, если он передается)
|
||
logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; // Оставляем targetBaseStats.resourceName, т.к. он точнее для лога
|
||
// Если урон был 0, и ресурса нет, можно уточнить лог
|
||
if(damageDealtDrain === 0 && potentialDrain > 0) logMsgDrain += ` Урон не прошел или равен нулю, ресурс не похищен.`;
|
||
}
|
||
logMsgDrain += tauntMessagePart; // Добавляем насмешку цели, если была
|
||
if (addToLogCallback) addToLogCallback(logMsgDrain, manaDrained > 0 || damageDealtDrain > 0 ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
|
||
} else {
|
||
console.warn(`applyAbilityEffect: Drain type ability ${ability?.name} used by non-Balard character ${casterState.characterKey}`);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Логика принятия решения для AI (Балард).
|
||
* @param {object} currentGameState - Текущее состояние игры.
|
||
* @param {object} gameDataForLogic - Данные игры.
|
||
* @param {object} configToUse - Конфигурация игры.
|
||
* @param {function} addToLogCallback - Функция для добавления лога.
|
||
* @returns {object} Объект с действием AI ({ actionType: 'attack' | 'ability', ability?: object }).
|
||
*/
|
||
function decideAiAction(currentGameState, gameDataForLogic = gameData, configToUse, addToLogCallback) {
|
||
const opponentState = currentGameState.opponent; // AI Балард всегда в слоте opponent
|
||
const playerState = currentGameState.player; // Игрок всегда в слоте player (в AI режиме)
|
||
|
||
// Убеждаемся, что это AI Балард
|
||
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 isBalardFullySilenced = opponentState.activeEffects.some(
|
||
eff => eff.isFullSilence && eff.turnsLeft > 0
|
||
);
|
||
|
||
if (isBalardFullySilenced) {
|
||
// AI под полным безмолвием просто атакует
|
||
// Лог о безмолвии и атаке в смятении добавляется в processAiTurn перед вызовом performAttack.
|
||
// decideAiAction просто возвращает действие.
|
||
return { actionType: 'attack' };
|
||
}
|
||
|
||
const availableActions = [];
|
||
const opponentAbilities = gameDataForLogic.opponentAbilities; // Способности Баларда
|
||
|
||
// Проверяем доступность способностей AI и добавляем их в список возможных действий с весом
|
||
// Вес определяет приоритет: выше вес -> выше шанс выбора (после сортировки)
|
||
|
||
const healAbility = opponentAbilities.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 });
|
||
}
|
||
|
||
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?.[silenceAbility.id] || 0) <= 0 && // Проверка общего КД
|
||
silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
||
const playerHpPercent = (playerState.currentHp / playerState.maxHp) * 100;
|
||
// Балард предпочитает безмолвие, если HP Елены не слишком низкое (позволяет ей лечиться, чтобы игра длилась дольше)
|
||
if (playerHpPercent > (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 60)) { // Используем порог для текстов Елены как пример
|
||
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?.[drainAbility.id] || 0) <= 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.type === 'ability') {
|
||
// Если способность требует проверки успеха (например, Безмолвие Баларда)
|
||
if (action.requiresSuccessCheck) {
|
||
if (Math.random() < action.successRate) {
|
||
// Успех, добавляем лог о попытке (чтобы было видно, что AI пытался)
|
||
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' };
|
||
}
|
||
// 'pass' не должен быть в доступных действиях, если атака всегда доступна
|
||
}
|
||
|
||
// Если все попытки выбрать способность или атаку провалились (очень маловероятно, если атака всегда в списке), пропуск хода
|
||
console.warn("[AI DEBUG] AI failed to select any action. Defaulting to pass.");
|
||
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не смог выбрать подходящее действие. Пропускает ход.`, type: configToUse.LOG_TYPE_INFO } };
|
||
}
|
||
|
||
/**
|
||
* Внутренняя проверка условий конца игры (основано на HP).
|
||
* @param {object} currentGameState - Текущее состояние игры.
|
||
* @param {object} configToUse - Конфигурация игры.
|
||
* @param {object} gameDataForLogic - Данные игры.
|
||
* @returns {boolean} true, если игра окончена, иначе false.
|
||
*/
|
||
function checkGameOverInternal(currentGameState, configToUse, gameDataForLogic = gameData) {
|
||
// Проверка на конец игры происходит только если gameState существует и игра еще не помечена как оконченная
|
||
if (!currentGameState || currentGameState.isGameOver) return currentGameState ? currentGameState.isGameOver : true;
|
||
|
||
// Убеждаемся, что оба бойца определены в gameState и не являются плейсхолдерами
|
||
// Проверка maxHp > 0 в gameState.opponent гарантирует, что оппонент не плейсхолдер
|
||
if (!currentGameState.player || !currentGameState.opponent || currentGameState.opponent.maxHp <= 0) {
|
||
// Если один из бойцов не готов (например, PvP игра ожидает второго игрока), игра не может закончиться по HP
|
||
return false;
|
||
}
|
||
|
||
|
||
const playerDead = currentGameState.player.currentHp <= 0;
|
||
const opponentDead = currentGameState.opponent.currentHp <= 0;
|
||
|
||
// Игра окончена, если один или оба бойца мертвы
|
||
return playerDead || opponentDead;
|
||
}
|
||
|
||
// Экспортируем все функции, которые используются в других модулях
|
||
module.exports = {
|
||
processEffects,
|
||
processDisabledAbilities,
|
||
processPlayerAbilityCooldowns,
|
||
updateBlockingStatus,
|
||
getRandomTaunt, // Экспортируем переименованную функцию
|
||
performAttack,
|
||
applyAbilityEffect,
|
||
decideAiAction,
|
||
checkGameOverInternal
|
||
}; |