bc/server/game/logic/combatLogic.js
2025-05-25 17:47:38 +03:00

357 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
// Предполагается, что gameLogic.getRandomTaunt и dataUtils будут доступны
// через параметры, передаваемые из GameInstance, или через объект gameLogic.
// const GAME_CONFIG_STATIC = require('../../core/config'); // Можно, если нужно
/**
* Обрабатывает базовую атаку одного бойца по другому.
* @param {object} attackerState - Состояние атакующего бойца из gameState.
* @param {object} defenderState - Состояние защищающегося бойца из gameState.
* @param {object} attackerBaseStats - Базовые статы атакующего.
* @param {object} defenderBaseStats - Базовые статы защищающегося.
* @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, // Добавлен dataUtils
getRandomTauntFunction // Добавлена функция для насмешек
) {
let damage = Math.floor(
attackerBaseStats.attackPower *
(configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)
);
let wasBlocked = false;
if (defenderState.isBlocking) {
const initialDamage = damage;
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
wasBlocked = true;
if (addToLogCallback) {
addToLogCallback(
`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`,
configToUse.LOG_TYPE_BLOCK
);
}
} else {
if (addToLogCallback) {
addToLogCallback(
`${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`,
configToUse.LOG_TYPE_DAMAGE
);
}
}
const actualDamageDealt = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage));
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
if (getRandomTauntFunction && dataUtils) {
let reactionTauntTrigger = null;
if (wasBlocked) {
reactionTauntTrigger = 'onOpponentAttackBlocked';
} else if (actualDamageDealt > 0) { // Если урон прошел
reactionTauntTrigger = 'onOpponentAttackHit';
}
// Можно добавить 'onOpponentAttackMissed' если actualDamageDealt === 0 и !wasBlocked
if (reactionTauntTrigger) {
const attackerFullData = dataUtils.getCharacterData(attackerState.characterKey);
if (attackerFullData) { // Убедимся, что данные атакующего есть
const reactionTaunt = getRandomTauntFunction(
defenderState.characterKey, // Кто говорит (защищающийся)
reactionTauntTrigger, // Триггер (onOpponentAttackBlocked или onOpponentAttackHit)
{}, // Контекст (пока пустой для этих реакций)
configToUse,
attackerFullData, // Оппонент для говорящего - это атакующий
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} checkIfActionWasSuccessfulFunction - Функция для проверки успеха действия (для контекстных насмешек).
*/
function applyAbilityEffect(
ability,
casterState,
targetState,
casterBaseStats,
targetBaseStats,
currentGameState,
addToLogCallback,
configToUse,
dataUtils, // Добавлен dataUtils
getRandomTauntFunction, // Добавлена функция для насмешек
checkIfActionWasSuccessfulFunction // Добавлена функция для проверки успеха
) {
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); // 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) {
// ... (логика полного безмолвия как у вас)
// Установите actionOutcomeForTaunt = 'success' или 'fail' если нужно
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 });
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:
// ... (логика дебаффа как у вас)
// Установите actionOutcomeForTaunt если нужно
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) 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 && abilityApplicationSucceeded) {
const casterFullData = dataUtils.getCharacterData(casterState.characterKey);
if (casterFullData) { // Убедимся, что данные кастера есть
let tauntContext = { abilityId: ability.id };
if (actionOutcomeForTaunt) { // Если для этой способности важен исход (success/fail)
tauntContext.outcome = actionOutcomeForTaunt;
} else if (checkIfActionWasSuccessfulFunction) {
// Если есть общая функция проверки успеха (менее специфично, чем actionOutcomeForTaunt)
// Это пример, вам нужно реализовать checkIfActionWasSuccessfulFunction
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
// tauntContext.outcome = success ? 'success' : 'fail';
}
const reactionTaunt = getRandomTauntFunction(
targetState.characterKey, // Кто говорит (цель способности)
'onOpponentAction', // Триггер
tauntContext, // Контекст: ID способности и исход (если нужен)
configToUse,
casterFullData, // Оппонент для говорящего - это кастер
currentGameState
);
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
}
}
}
}
/**
* Проверяет валидность использования способности.
* @param {object} ability - Объект способности.
* @param {object} casterState - Состояние кастера.
* @param {object} targetState - Состояние цели.
* @param {object} configToUse - Конфигурация игры.
* @returns {{isValid: boolean, reason: string|null}} Результат проверки.
*/
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)) {
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
};