// /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 };