Some checks failed
Deploy Project BC / deploy (push) Failing after 32s
472 lines
31 KiB
JavaScript
472 lines
31 KiB
JavaScript
// /server/game/logic/combatLogic.js
|
||
|
||
// GAME_CONFIG и dataUtils будут передаваться в функции как параметры.
|
||
// effectsLogic может потребоваться для импорта, если updateBlockingStatus используется здесь напрямую,
|
||
// но в вашем GameInstance.js он вызывается отдельно.
|
||
// const effectsLogic = require('./effectsLogic'); // Если нужно
|
||
|
||
/**
|
||
* Обрабатывает базовую атаку одного бойца по другому.
|
||
* @param {object} attackerState - Состояние атакующего бойца из gameState.
|
||
* @param {object} defenderState - Состояние защищающегося бойца из gameState.
|
||
* @param {object} attackerBaseStats - Базовые статы атакующего (из dataUtils.getCharacterBaseStats).
|
||
* @param {object} defenderBaseStats - Базовые статы защищающегося (из dataUtils.getCharacterBaseStats).
|
||
* @param {object} currentGameState - Текущее полное состояние игры.
|
||
* @param {function} addToLogCallback - Функция для добавления сообщений в лог игры.
|
||
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
||
* @param {object} dataUtils - Утилиты для доступа к данным игры.
|
||
* @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt, переданная для использования.
|
||
*/
|
||
function performAttack(
|
||
attackerState,
|
||
defenderState,
|
||
attackerBaseStats,
|
||
defenderBaseStats,
|
||
currentGameState,
|
||
addToLogCallback,
|
||
configToUse,
|
||
dataUtils,
|
||
getRandomTauntFunction
|
||
) {
|
||
// Расчет базового урона с вариацией
|
||
let damage = Math.floor(
|
||
attackerBaseStats.attackPower *
|
||
(configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)
|
||
);
|
||
let wasBlocked = false;
|
||
let attackBonusesLog = []; // Для сбора информации о бонусах к атаке
|
||
|
||
// --- ПРОВЕРКА И ПРИМЕНЕНИЕ БОНУСА ОТ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
||
const delayedAttackBuff = attackerState.activeEffects.find(eff =>
|
||
eff.isDelayed &&
|
||
(eff.id === configToUse.ABILITY_ID_NATURE_STRENGTH || eff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) &&
|
||
eff.turnsLeft > 0 &&
|
||
!eff.justCast
|
||
);
|
||
|
||
if (delayedAttackBuff) {
|
||
console.log(`[CombatLogic performAttack] Found active delayed buff: ${delayedAttackBuff.name} for ${attackerState.name}`);
|
||
|
||
let damageBonus = 0;
|
||
// Если бы были прямые бонусы к урону атаки от этих баффов, они бы рассчитывались здесь
|
||
// Например:
|
||
// if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH && configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS) {
|
||
// damageBonus = configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS;
|
||
// } else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK && configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS) {
|
||
// damageBonus = configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS;
|
||
// }
|
||
|
||
if (damageBonus > 0) {
|
||
damage += damageBonus;
|
||
attackBonusesLog.push(`урон +${damageBonus} от "${delayedAttackBuff.name}"`);
|
||
}
|
||
|
||
let resourceRegenConfigKey = null;
|
||
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
|
||
resourceRegenConfigKey = 'NATURE_STRENGTH_MANA_REGEN';
|
||
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
|
||
resourceRegenConfigKey = 'ALMAGEST_DARK_ENERGY_REGEN'; // Предположительный ключ
|
||
}
|
||
|
||
if (resourceRegenConfigKey && configToUse[resourceRegenConfigKey]) {
|
||
const regenAmount = configToUse[resourceRegenConfigKey];
|
||
const actualRegen = Math.min(regenAmount, attackerBaseStats.maxResource - attackerState.currentResource);
|
||
if (actualRegen > 0) {
|
||
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
|
||
if (addToLogCallback) {
|
||
addToLogCallback(
|
||
`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от "${delayedAttackBuff.name}"!`,
|
||
configToUse.LOG_TYPE_HEAL
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// --- КОНЕЦ ПРОВЕРКИ И ПРИМЕНЕНИЯ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
||
|
||
// Проверка на блок
|
||
if (defenderState.isBlocking) {
|
||
const initialDamage = damage;
|
||
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||
wasBlocked = true;
|
||
if (addToLogCallback) {
|
||
let blockLogMsg = `🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`;
|
||
if (attackBonusesLog.length > 0) {
|
||
blockLogMsg += ` (${attackBonusesLog.join(', ')})`;
|
||
}
|
||
addToLogCallback(blockLogMsg, configToUse.LOG_TYPE_BLOCK);
|
||
}
|
||
} else {
|
||
if (addToLogCallback) {
|
||
let hitLogMsg = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
|
||
if (attackBonusesLog.length > 0) {
|
||
hitLogMsg += ` (${attackBonusesLog.join(', ')})`;
|
||
}
|
||
addToLogCallback(hitLogMsg, configToUse.LOG_TYPE_DAMAGE);
|
||
}
|
||
}
|
||
|
||
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||
const actualDamageDealtToHp = Math.min(defenderState.currentHp, damage); // Сколько HP реально отнято (не может быть больше текущего HP)
|
||
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
||
|
||
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
|
||
if (getRandomTauntFunction && dataUtils) {
|
||
let subTriggerForTaunt = null;
|
||
if (wasBlocked) {
|
||
subTriggerForTaunt = 'attackBlocked';
|
||
} else if (actualDamageDealtToHp > 0) { // Если не было блока, но был нанесен урон
|
||
subTriggerForTaunt = 'attackHits';
|
||
}
|
||
// Можно добавить еще условие для промаха, если урон = 0 и не было блока (и actualDamageDealtToHp === 0)
|
||
// else if (damage <= 0 && !wasBlocked) { subTriggerForTaunt = 'attackMissed'; } // Если есть такой триггер
|
||
|
||
if (subTriggerForTaunt) {
|
||
const attackerFullDataForTaunt = dataUtils.getCharacterData(attackerState.characterKey);
|
||
if (attackerFullDataForTaunt) {
|
||
const reactionTaunt = getRandomTauntFunction(
|
||
defenderState.characterKey, // Говорящий (защитник)
|
||
'onOpponentAction', // Главный триггер
|
||
subTriggerForTaunt, // Подтриггер: 'attackBlocked' или 'attackHits'
|
||
configToUse,
|
||
attackerFullDataForTaunt, // Оппонент (атакующий) для говорящего
|
||
currentGameState
|
||
);
|
||
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||
addToLogCallback(`${defenderState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* применяет111 эффект способности.
|
||
* @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} dataUtils - Утилиты для доступа к данным игры.
|
||
* @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt.
|
||
* @param {function|null} checkIfActionWasSuccessfulFunction - (Опционально) Функция для проверки успеха действия для контекстных насмешек.
|
||
*/
|
||
function applyAbilityEffect(
|
||
ability,
|
||
casterState,
|
||
targetState,
|
||
casterBaseStats,
|
||
targetBaseStats,
|
||
currentGameState,
|
||
addToLogCallback,
|
||
configToUse,
|
||
dataUtils,
|
||
getRandomTauntFunction,
|
||
checkIfActionWasSuccessfulFunction // Пока не используется активно, outcome определяется внутри
|
||
) {
|
||
let abilityApplicationSucceeded = true; // Флаг общего успеха применения способности
|
||
let actionOutcomeForTaunt = null; // 'success' или 'fail' для специфичных насмешек (например, Безмолвие Баларда)
|
||
|
||
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 = Math.round(casterState.currentHp + actualHeal);
|
||
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет111 "${ability.name}" и восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
|
||
actionOutcomeForTaunt = 'success'; // Для реакции оппонента, если таковая есть на хил
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет111 "${ability.name}", но не получает лечения (HP уже полное или хил = 0).`, configToUse.LOG_TYPE_INFO);
|
||
abilityApplicationSucceeded = false;
|
||
actionOutcomeForTaunt = 'fail';
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DAMAGE:
|
||
let damage = Math.floor(ability.power * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
||
let wasAbilityBlocked = false;
|
||
let actualDamageDealtByAbility = 0;
|
||
|
||
if (targetState.isBlocking) {
|
||
const initialDamage = damage;
|
||
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||
wasAbilityBlocked = true;
|
||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`, configToUse.LOG_TYPE_BLOCK);
|
||
}
|
||
|
||
actualDamageDealtByAbility = Math.min(targetState.currentHp, damage);
|
||
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
||
|
||
if (addToLogCallback && !wasAbilityBlocked) {
|
||
addToLogCallback(`💥 ${casterBaseStats.name} применяет111 "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE);
|
||
}
|
||
|
||
if (damage <= 0 && !wasAbilityBlocked) { // Если урон нулевой и не было блока (например, из-за резистов, которых пока нет)
|
||
abilityApplicationSucceeded = false;
|
||
actionOutcomeForTaunt = 'fail';
|
||
} else if (wasAbilityBlocked) {
|
||
actionOutcomeForTaunt = 'blocked'; // Специальный исход для реакции на блок способности
|
||
} else if (actualDamageDealtByAbility > 0) {
|
||
actionOutcomeForTaunt = 'hit'; // Специальный исход для реакции на попадание способностью
|
||
} else {
|
||
actionOutcomeForTaunt = 'fail'; // Если урон 0 и не было блока (например цель уже мертва и 0 хп)
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_BUFF:
|
||
let effectDescriptionBuff = ability.description;
|
||
if (typeof ability.descriptionFunction === 'function') {
|
||
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats); // targetBaseStats здесь может быть casterBaseStats, если бафф на себя
|
||
}
|
||
// Обычно баффы накладываются на кастера
|
||
casterState.activeEffects.push({
|
||
id: ability.id, name: ability.name, description: effectDescriptionBuff,
|
||
type: ability.type, duration: ability.duration,
|
||
turnsLeft: ability.duration,
|
||
grantsBlock: !!ability.grantsBlock,
|
||
isDelayed: !!ability.isDelayed,
|
||
justCast: true
|
||
});
|
||
if (ability.grantsBlock && casterState.activeEffects.find(e => e.id === ability.id && e.grantsBlock)) {
|
||
// Требуется effectsLogic.updateBlockingStatus(casterState);
|
||
// но GameInstance вызывает его в switchTurn, так что здесь можно не дублировать, если эффект не мгновенный
|
||
}
|
||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
|
||
actionOutcomeForTaunt = 'success'; // Для реакции оппонента, если бафф на себя
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DISABLE:
|
||
// Общее "полное безмолвие" от Елены или Альмагест
|
||
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
|
||
const effectIdFullSilence = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
|
||
if (!targetState.activeEffects.some(e => e.id === effectIdFullSilence)) {
|
||
targetState.activeEffects.push({
|
||
id: effectIdFullSilence, 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} применяет111 "${ability.name}" на ${targetBaseStats.name}! Способности цели заблокированы на ${ability.effectDuration} хода!`, configToUse.LOG_TYPE_EFFECT);
|
||
actionOutcomeForTaunt = 'success';
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO);
|
||
abilityApplicationSucceeded = false;
|
||
actionOutcomeForTaunt = 'fail';
|
||
}
|
||
}
|
||
// Специальное Безмолвие Баларда
|
||
else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') {
|
||
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
|
||
actionOutcomeForTaunt = success ? 'success' : 'fail'; // Этот outcome используется в tauntLogic
|
||
if (success) {
|
||
const targetAbilitiesList = dataUtils.getCharacterAbilities(targetState.characterKey);
|
||
const availableAbilitiesToSilence = targetAbilitiesList.filter(pa =>
|
||
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
|
||
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`) &&
|
||
pa.id !== configToUse.ABILITY_ID_NONE // Исключаем "пустую" абилку, если она есть
|
||
);
|
||
if (availableAbilitiesToSilence.length > 0) {
|
||
const abilityToSilence = availableAbilitiesToSilence[Math.floor(Math.random() * availableAbilitiesToSilence.length)];
|
||
const turns = configToUse.SILENCE_DURATION;
|
||
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 });
|
||
targetState.activeEffects.push({
|
||
id: `playerSilencedOn_${abilityToSilence.id}`, name: `Безмолвие: ${abilityToSilence.name}`,
|
||
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
|
||
type: configToUse.ACTION_TYPE_DISABLE, sourceAbilityId: ability.id,
|
||
duration: turns, turnsLeft: turns + 1, justCast: true
|
||
});
|
||
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" у ${targetBaseStats.name} заблокировано на ${turns} хода!`, configToUse.LOG_TYPE_EFFECT);
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!`, configToUse.LOG_TYPE_INFO);
|
||
actionOutcomeForTaunt = 'fail'; // Переопределяем, т.к. нечего было глушить
|
||
}
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!`, configToUse.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DEBUFF:
|
||
const effectIdDebuff = 'effect_' + ability.id; // Уникальный ID для дебаффа на цели
|
||
if (!targetState.activeEffects.some(e => e.id === effectIdDebuff)) {
|
||
let effectDescriptionDebuff = ability.description;
|
||
if (typeof ability.descriptionFunction === 'function') {
|
||
effectDescriptionDebuff = ability.descriptionFunction(configToUse, targetBaseStats);
|
||
}
|
||
targetState.activeEffects.push({
|
||
id: effectIdDebuff, name: ability.name, description: effectDescriptionDebuff,
|
||
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}! Эффект продлится ${ability.effectDuration} хода.`, configToUse.LOG_TYPE_EFFECT);
|
||
actionOutcomeForTaunt = 'success';
|
||
} else {
|
||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO);
|
||
abilityApplicationSucceeded = false;
|
||
actionOutcomeForTaunt = 'fail';
|
||
}
|
||
break;
|
||
|
||
case configToUse.ACTION_TYPE_DRAIN: // Пример для Манадрейна Баларда
|
||
if (casterState.characterKey === 'balard' && ability.id === configToUse.ABILITY_ID_BALARD_MANA_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);
|
||
}
|
||
damageDealtDrain = Math.max(0, baseDamageDrain);
|
||
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 || 0)); // Убедимся, что фактор есть
|
||
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
|
||
if (actualHealGain > 0) {
|
||
casterState.currentHp = Math.round(casterState.currentHp + actualHealGain);
|
||
healthGained = actualHealGain;
|
||
}
|
||
}
|
||
|
||
let logMsgDrain = `⚡ ${casterBaseStats.name} применяет1111111 "${ability.name}"! `;
|
||
if (damageDealtDrain > 0) logMsgDrain += `Наносит ${damageDealtDrain} урона ${targetBaseStats.name}. `;
|
||
if (manaDrained > 0) {
|
||
logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name}`;
|
||
if(healthGained > 0) logMsgDrain += ` и исцеляется на ${healthGained} HP!`; else logMsgDrain += `!`;
|
||
} else if (damageDealtDrain > 0) {
|
||
logMsgDrain += `${targetBaseStats.name} не имеет ${targetBaseStats.resourceName} для похищения.`;
|
||
} else {
|
||
logMsgDrain += `Не удалось ничего похитить у ${targetBaseStats.name}.`;
|
||
}
|
||
|
||
if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
|
||
|
||
if (manaDrained <= 0 && damageDealtDrain <= 0 && healthGained <= 0) {
|
||
abilityApplicationSucceeded = false;
|
||
actionOutcomeForTaunt = 'fail';
|
||
} else {
|
||
actionOutcomeForTaunt = 'success';
|
||
}
|
||
}
|
||
break;
|
||
|
||
default:
|
||
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
|
||
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type} для способности ${ability?.id}`);
|
||
abilityApplicationSucceeded = false;
|
||
actionOutcomeForTaunt = 'fail';
|
||
}
|
||
|
||
// --- Насмешка от цели (targetState) в ответ на применение способности оппонентом (casterState) ---
|
||
// Вызываем только если способность не была нацелена на самого себя И есть функция насмешек
|
||
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id) {
|
||
const casterFullDataForTaunt = dataUtils.getCharacterData(casterState.characterKey);
|
||
if (casterFullDataForTaunt) {
|
||
let tauntContext = { abilityId: ability.id };
|
||
|
||
// Если для этой способности был определен исход (например, для безмолвия Баларда, или попадание/блок урона)
|
||
// Используем actionOutcomeForTaunt, который мы установили в switch-case выше
|
||
if (actionOutcomeForTaunt === 'success' || actionOutcomeForTaunt === 'fail' || actionOutcomeForTaunt === 'blocked' || actionOutcomeForTaunt === 'hit') {
|
||
tauntContext.outcome = actionOutcomeForTaunt;
|
||
}
|
||
// Для способностей типа DAMAGE, 'blocked' и 'hit' будут ключами в taunts.js (например, Elena onOpponentAction -> ABILITY_ID_ALMAGEST_DAMAGE -> blocked: [...])
|
||
// Это не стандартные 'attackBlocked' и 'attackHits', а специфичные для реакции на *способность*
|
||
// Если вы хотите использовать общие 'attackBlocked'/'attackHits' и для способностей, вам нужно будет изменить логику в taunts.js
|
||
// или передавать здесь другие subTrigger'ы, если способность заблокирована/попала.
|
||
|
||
const reactionTaunt = getRandomTauntFunction(
|
||
targetState.characterKey, // Кто говорит (цель способности)
|
||
'onOpponentAction', // Триггер
|
||
tauntContext, // Контекст: ID способности кастера (оппонента) и, возможно, outcome
|
||
configToUse,
|
||
casterFullDataForTaunt, // Оппонент для говорящего - это кастер
|
||
currentGameState
|
||
);
|
||
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Проверяет валидность использования способности.
|
||
* @param {object} ability - Объект способности.
|
||
* @param {object} casterState - Состояние бойца, который пытается применить способность.
|
||
* @param {object} targetState - Состояние цели (может быть тем же, что и casterState).
|
||
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
||
* @returns {object} - { isValid: boolean, reason: string|null }
|
||
*/
|
||
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
||
if (!ability) return { isValid: false, reason: "Способность не найдена." };
|
||
|
||
if (casterState.currentResource < ability.cost) {
|
||
return { isValid: false, reason: `${casterState.name} пытается применить "${ability.name}", но не хватает ${casterState.resourceName} (${casterState.currentResource}/${ability.cost})!` };
|
||
}
|
||
|
||
if ((casterState.abilityCooldowns?.[ability.id] || 0) > 0) {
|
||
return { isValid: false, reason: `"${ability.name}" еще на перезарядке (${casterState.abilityCooldowns[ability.id]} х.).` };
|
||
}
|
||
|
||
// Специальные кулдауны для Баларда
|
||
if (casterState.characterKey === 'balard') {
|
||
if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && (casterState.silenceCooldownTurns || 0) > 0) {
|
||
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке (${casterState.silenceCooldownTurns} х.).` };
|
||
}
|
||
if (ability.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN && (casterState.manaDrainCooldownTurns || 0) > 0) {
|
||
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке (${casterState.manaDrainCooldownTurns} х.).` };
|
||
}
|
||
}
|
||
|
||
// Проверка на безмолвие
|
||
const isCasterFullySilenced = casterState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||
const isAbilitySpecificallySilenced = casterState.disabledAbilities?.some(dis => dis.abilityId === ability.id && dis.turnsLeft > 0);
|
||
if (isCasterFullySilenced) {
|
||
return { isValid: false, reason: `${casterState.name} не может использовать способности из-за полного безмолвия!` };
|
||
}
|
||
if (isAbilitySpecificallySilenced) {
|
||
const specificSilenceEffect = casterState.disabledAbilities.find(dis => dis.abilityId === ability.id);
|
||
return { isValid: false, reason: `Способность "${ability.name}" у ${casterState.name} временно заблокирована (${specificSilenceEffect.turnsLeft} х.)!` };
|
||
}
|
||
|
||
|
||
// Проверка наложения баффа, который уже активен (кроме обновляемых)
|
||
if (ability.type === configToUse.ACTION_TYPE_BUFF && casterState.activeEffects.some(e => e.id === ability.id)) {
|
||
// Исключение для "отложенных" баффов, которые можно обновлять (например, Сила Природы)
|
||
if (!ability.isDelayed) { // Если isDelayed не true, то нельзя обновлять.
|
||
return { isValid: false, reason: `Эффект "${ability.name}" уже активен у ${casterState.name}!` };
|
||
}
|
||
}
|
||
|
||
// Проверка наложения дебаффа, который уже активен на цели
|
||
const isTargetedDebuff = ability.type === configToUse.ACTION_TYPE_DEBUFF ||
|
||
(ability.type === configToUse.ACTION_TYPE_DISABLE && ability.id !== configToUse.ABILITY_ID_BALARD_SILENCE); // Безмолвие Баларда может пытаться наложиться повторно (и провалиться)
|
||
|
||
if (isTargetedDebuff && targetState.id !== casterState.id) { // Убедимся, что это не бафф на себя, проверяемый как дебафф
|
||
const effectIdToCheck = (ability.type === configToUse.ACTION_TYPE_DISABLE && ability.id !== configToUse.ABILITY_ID_BALARD_SILENCE) ?
|
||
(ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest') :
|
||
('effect_' + ability.id);
|
||
|
||
if (targetState.activeEffects.some(e => e.id === effectIdToCheck)) {
|
||
return { isValid: false, reason: `Эффект "${ability.name}" уже наложен на ${targetState.name}!` };
|
||
}
|
||
}
|
||
|
||
return { isValid: true, reason: null };
|
||
}
|
||
|
||
|
||
module.exports = {
|
||
performAttack,
|
||
applyAbilityEffect,
|
||
checkAbilityValidity
|
||
}; |