bc/server/game/logic/combatLogic.js

408 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /server/game/logic/combatLogic.js
// GAME_CONFIG и dataUtils будут передаваться в функции как параметры.
/**
* Обрабатывает базовую атаку одного бойца по другому.
* @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}`);
// 1. Применяем бонус к урону (если он есть в конфиге/данных эффекта)
let damageBonus = 0;
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
// Предположим, что Сила Природы НЕ дает прямого бонуса к урону атаки, а только реген маны.
// Если бы давала, то: damageBonus = configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS || 0;
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
// Аналогично для Альмагест
// damageBonus = configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS || 0;
}
if (damageBonus > 0) {
damage += damageBonus;
attackBonusesLog.push(`урон +${damageBonus} от "${delayedAttackBuff.name}"`);
}
// 2. Восстановление ресурса (для Силы Природы / Усиления Тьмой)
// Этот бонус (восстановление ресурса) срабатывает при каждой атаке, пока эффект активен
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
);
}
// Не добавляем в attackBonusesLog, т.к. это отдельное событие, уже залогированное
}
}
// Важно: НЕ МЕНЯЕМ здесь delayedAttackBuff.turnsLeft и НЕ УДАЛЯЕМ эффект.
// Его длительность будет уменьшаться в effectsLogic.processEffects каждый ход владельца эффекта.
}
// --- КОНЕЦ ПРОВЕРКИ И ПРИМЕНЕНИЯ ОТЛОЖЕННОГО БАФФА АТАКИ ---
// Проверка на блок
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 = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage)); // Сколько HP реально отнято
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
if (getRandomTauntFunction && dataUtils) {
let reactionTauntTrigger = null;
if (wasBlocked) {
reactionTauntTrigger = 'onOpponentAttackBlocked';
} else if (actualDamageDealtToHp > 0) {
reactionTauntTrigger = 'onOpponentAttackHit';
}
// Можно добавить еще условие для промаха, если урон = 0 и не было блока
if (reactionTauntTrigger) {
const attackerFullDataForTaunt = dataUtils.getCharacterData(attackerState.characterKey);
if (attackerFullDataForTaunt) {
const reactionTaunt = getRandomTauntFunction(
defenderState.characterKey,
reactionTauntTrigger,
{},
configToUse,
attackerFullDataForTaunt, // Оппонент для говорящего (защитника) - это атакующий
currentGameState
);
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
addToLogCallback(`${defenderState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
}
}
}
}
}
/**
* Применяет эффект способности.
* @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} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
} else {
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.`, configToUse.LOG_TYPE_INFO);
abilityApplicationSucceeded = false;
}
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;
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);
}
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
if (addToLogCallback && !wasAbilityBlocked) {
addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE);
}
if (damage <= 0 && !wasAbilityBlocked) abilityApplicationSucceeded = false;
break;
case configToUse.ACTION_TYPE_BUFF:
let effectDescriptionBuff = ability.description;
if (typeof ability.descriptionFunction === 'function') {
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats);
}
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) require('./effectsLogic').updateBlockingStatus(casterState);
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
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} применяет "${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';
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}`)
);
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 }); // +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;
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}! Ресурс будет сжигаться.`, 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') {
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);
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
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 logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
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;
}
break;
default:
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type}`);
abilityApplicationSucceeded = false;
}
// --- Насмешка от цели (targetState) в ответ на применение способности ---
// Вызываем только если способность не была нацелена на самого себя
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id) {
const casterFullDataForTaunt = dataUtils.getCharacterData(casterState.characterKey);
if (casterFullDataForTaunt) {
let tauntContext = { abilityId: ability.id };
// Если для этой способности был определен исход (например, для безмолвия Баларда), используем его
if (actionOutcomeForTaunt) {
tauntContext.outcome = actionOutcomeForTaunt;
}
// Здесь можно было бы вызвать checkIfActionWasSuccessfulFunction, если бы он был и нужен для других способностей
// else if (checkIfActionWasSuccessfulFunction) {
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
// tauntContext.outcome = success ? 'success' : 'fail';
// }
// Вызываем насмешку, только если основное применение способности не считается полным провалом (опционально)
// Либо всегда вызываем, и пусть tauntLogic решает, есть ли реакция на "провальную" абилку
// if (abilityApplicationSucceeded || actionOutcomeForTaunt === 'fail') { // Например, реагируем даже на провал Эха Безмолвия
const reactionTaunt = getRandomTauntFunction(
targetState.characterKey, // Кто говорит (цель способности)
'onOpponentAction', // Триггер
tauntContext, // Контекст: ID способности и, возможно, outcome
configToUse,
casterFullDataForTaunt, // Оппонент для говорящего - это кастер
currentGameState
);
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
}
// }
}
}
}
/**
* Проверяет валидность использования способности.
*/
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
// ... (код checkAbilityValidity без изменений, как вы предоставили) ...
if (!ability) return { isValid: false, reason: "Способность не найдена." };
if (casterState.currentResource < ability.cost) {
return { isValid: false, reason: `${casterState.name} пытается применить "${ability.name}", но не хватает ${casterState.resourceName}!` };
}
if ((casterState.abilityCooldowns?.[ability.id] || 0) > 0) {
return { isValid: false, reason: `"${ability.name}" еще на перезарядке.` };
}
if (casterState.characterKey === 'balard') {
if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && (casterState.silenceCooldownTurns || 0) > 0) {
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
}
if (ability.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN && (casterState.manaDrainCooldownTurns || 0) > 0) {
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
}
}
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 || isAbilitySpecificallySilenced) {
return { isValid: false, reason: `${casterState.name} не может использовать способности из-за безмолвия!` };
}
if (ability.type === configToUse.ACTION_TYPE_BUFF && casterState.activeEffects.some(e => e.id === ability.id)) {
// Исключение для Силы Природы и Усиления Тьмой - их можно обновлять, если isDelayed
if (!ability.isDelayed) {
return { isValid: false, reason: `Эффект "${ability.name}" уже активен!` };
}
}
const isTargetedDebuff = ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF;
if (isTargetedDebuff && targetState.activeEffects.some(e => e.id === 'effect_' + ability.id)) {
return { isValid: false, reason: `Эффект "${ability.name}" уже наложен на ${targetState.name}!` };
}
return { isValid: true, reason: null };
}
module.exports = {
performAttack,
applyAbilityEffect,
checkAbilityValidity
};