bc/server/game/logic/combatLogic.js
2025-05-18 10:50:38 +03:00

313 lines
23 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 и gameData/dataUtils будут передаваться в функции как параметры.
// const GAME_CONFIG_STATIC = require('../../core/config'); // Можно импортировать для внутренних нужд, если не все приходит через параметры
/**
* Обрабатывает базовую атаку одного бойца по другому.
* @param {object} attackerState - Состояние атакующего бойца из gameState.
* @param {object} defenderState - Состояние защищающегося бойца из gameState.
* @param {object} attackerBaseStats - Базовые статы атакующего (из dataUtils.getCharacterBaseStats).
* @param {object} defenderBaseStats - Базовые статы защищающегося (из dataUtils.getCharacterBaseStats).
* @param {object} currentGameState - Текущее полное состояние игры (для getRandomTaunt).
* @param {function} addToLogCallback - Функция для добавления сообщений в лог игры.
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
* @param {object} defenderFullData - Полные данные защищающегося персонажа (baseStats, abilities) из dataUtils.getCharacterData(defenderKey), для getRandomTaunt.
*/
function performAttack(
attackerState,
defenderState,
attackerBaseStats,
defenderBaseStats,
currentGameState, // Добавлен для контекста насмешек
addToLogCallback,
configToUse,
defenderFullData // Добавлен для контекста насмешек цели
) {
// Расчет базового урона с вариацией
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 здесь или передаем как параметр, если он в другом файле logic
// Предположим, getRandomTaunt доступен в gameLogic (который будет передан или импортирован)
// Для примера, если бы он был в этом же файле или импортирован:
// const blockTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameData, currentGameState);
// Поскольку getRandomTaunt теперь в gameLogic.js, он должен быть вызван оттуда или передан.
// В GameInstance.js мы вызываем gameLogic.getRandomTaunt, так что здесь это дублирование.
// Лучше, чтобы GameInstance сам обрабатывал насмешки или передавал их как результат.
// Для простоты здесь оставим, но это кандидат на рефакторинг вызова насмешек в GameInstance.
// Однако, если defenderFullData передается, мы можем вызвать его, предполагая, что gameLogic.getRandomTaunt будет импортирован
// или доступен в объекте gameLogic, переданном в GameInstance.
// const blockTaunt = require('./index').getRandomTaunt(...) // Пример циклической зависимости, так не надо
// Будем считать, что GameInstance готовит насмешку заранее или эта функция вызывается с уже готовой насмешкой.
// Либо, если getRandomTaunt - это часть 'gameLogic' объекта, то:
// const blockTaunt = gameLogicFunctions.getRandomTaunt(...)
// Сейчас для простоты оставим вызов, но это архитектурный момент.
// Предположим, что gameLogic.getRandomTaunt доступен через какой-то объект, например, `sharedLogic`
}
if (addToLogCallback) {
addToLogCallback(
`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`,
configToUse.LOG_TYPE_BLOCK
);
}
} else {
// Насмешка при попадании также должна обрабатываться централизованно или передаваться
if (addToLogCallback) {
addToLogCallback(
`${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.${tauntMessagePart}`,
configToUse.LOG_TYPE_DAMAGE
);
}
}
// Применяем урон, убеждаемся, что HP не ниже нуля
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
}
/**
* Применяет эффект способности (урон, лечение, наложение баффа/дебаффа и т.д.).
* Насмешки, связанные с самим КАСТОМ способности (selfCastAbility), должны быть обработаны до вызова этой функции.
* Насмешки, связанные с РЕАКЦИЕЙ цели на эффект, могут быть обработаны здесь или после.
* @param {object} ability - Объект способности.
* @param {object} casterState - Состояние бойца, применившего способность.
* @param {object} targetState - Состояние цели способности.
* @param {object} casterBaseStats - Базовые статы кастера.
* @param {object} targetBaseStats - Базовые статы цели.
* @param {object} currentGameState - Текущее полное состояние игры (для getRandomTaunt, если он здесь вызывается).
* @param {function} addToLogCallback - Функция для добавления лога.
* @param {object} configToUse - Конфигурация игры.
* @param {object} targetFullData - Полные данные цели (baseStats, abilities) для getRandomTaunt.
*/
function applyAbilityEffect(
ability,
casterState,
targetState,
casterBaseStats,
targetBaseStats,
currentGameState,
addToLogCallback,
configToUse,
targetFullData // Для насмешек цели
) {
let tauntMessagePart = ""; // Для насмешки цели
// Насмешка цели (если это Елена/Альмагест) на применение способности противником
// Этот вызов лучше делать в GameInstance, передавая результат сюда, или эта функция должна иметь доступ к getRandomTaunt
// if ((targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') && casterState.id !== targetState.id) {
// const reactionTaunt = require('./index').getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id }, configToUse, targetFullData, currentGameState);
// if (reactionTaunt !== "(Молчание)") tauntMessagePart = ` (${reactionTaunt})`;
// }
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!${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 (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
}
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
if (addToLogCallback && !targetState.isBlocking) {
addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!${tauntMessagePart}`, configToUse.LOG_TYPE_DAMAGE);
}
break;
case configToUse.ACTION_TYPE_BUFF:
// Проверка на уже активный бафф должна быть сделана до вызова этой функции (в GameInstance)
let effectDescriptionBuff = ability.description;
if (typeof ability.descriptionFunction === 'function') {
// Для описания баффа может потребоваться информация о противнике (цели баффа, если бафф накладывается на другого)
// В данном случае, баффы накладываются на себя, так что targetBaseStats не всегда релевантен для описания.
// Передаем targetBaseStats (оппонента кастера), если описание функции его ожидает.
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}"!${tauntMessagePart}`, 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} хода и наносится урон!${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;
// Насмешка цели на успех/провал должна быть обработана в GameInstance, т.к. результат известен только здесь
if (success) {
const targetAbilitiesList = require('../../data/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 });
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} хода!${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:
// Логика для 'Печать Слабости' / 'Проклятие Увядания'
if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_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}! Ресурс будет сжигаться.${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;
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} для похищения.`;
logMsgDrain += tauntMessagePart;
if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
}
break;
default:
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type}`);
}
}
/**
* Проверяет валидность использования способности (ресурс, КД, безмолвие и т.д.).
* @param {object} ability - Объект способности.
* @param {object} casterState - Состояние кастера.
* @param {object} targetState - Состояние цели.
* @param {object} configToUse - Конфигурация игры.
* @returns {{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}!` };
}
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)) {
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 // Экспортируем новую функцию
};