bc/server_modules/gameLogic.js
2025-05-13 04:14:01 +00:00

510 lines
32 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_modules/gameLogic.js
const GAME_CONFIG = require('./config');
const gameData = require('./data'); // Загружаем один раз на уровне модуля
// --- Вспомогательные Функции ---
/**
* Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода.
* @param {Array} effectsArray - Массив активных эффектов бойца.
* @param {Object} ownerState - Состояние бойца (currentHp, currentResource и т.д.).
* @param {Object} ownerBaseStats - Базовые статы бойца (включая characterKey).
* @param {String} ownerId - Технический ID слота бойца ('player' или 'opponent').
* @param {Object} currentGameState - Полное состояние игры.
* @param {Function} addToLogCallback - Функция для добавления сообщений в лог игры.
* @param {Object} configToUse - Конфигурационный объект игры.
* @param {Object} gameDataForLogic - Полный объект gameData (для доступа к способностям и т.д.).
* @returns {void} - Модифицирует effectsArray и ownerState напрямую.
*/
function processEffects(effectsArray, ownerState, ownerBaseStats, ownerId, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
if (!effectsArray) return;
const ownerName = ownerBaseStats.name;
let effectsToRemoveIndexes = [];
for (let i = 0; i < effectsArray.length; i++) {
const eff = effectsArray[i];
const isNatureStrengthEffect = eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
// if (isNatureStrengthEffect) { // Отладочный лог
// console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect: ${eff.name}, justCast (before): ${eff.justCast}, turnsLeft (before): ${eff.turnsLeft}`);
// }
// --- Обработка эффектов с действием каждый ход (ДО уменьшения turnsLeft, если justCast === false) ---
if (!eff.justCast) { // Эффекты, которые тикают, не должны тикать в ход наложения
if (eff.isFullSilence && eff.power && typeof eff.power === 'number' && eff.power > 0) {
const damage = eff.power;
ownerState.currentHp = Math.max(0, ownerState.currentHp - damage);
if (addToLogCallback) addToLogCallback(`😵 Эффект "${eff.name}" наносит ${damage} урона ${ownerName}!`, configToUse.LOG_TYPE_DAMAGE);
}
if ((eff.id === 'effect_' + configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || eff.id === 'effect_' + configToUse.ABILITY_ID_ALMAGEST_DEBUFF) && eff.power > 0) {
const resourceToBurn = eff.power;
if (ownerState.currentResource > 0) {
const actualBurn = Math.min(ownerState.currentResource, resourceToBurn);
ownerState.currentResource = Math.max(0, ownerState.currentResource - actualBurn);
if (addToLogCallback) addToLogCallback(`🔥 Эффект "${eff.name}" сжигает ${actualBurn} ${ownerBaseStats.resourceName} у ${ownerName}!`, configToUse.LOG_TYPE_EFFECT);
}
}
}
// --- Уменьшение длительности эффекта ---
if (eff.justCast) {
eff.justCast = false;
} else {
// Не уменьшаем turnsLeft для эффектов, которые должны длиться до следующей атаки
// и не имеют фиксированного числа ходов (таких как Сила Природы, если бы она так работала).
// В нашем случае Сила Природы имеет duration, поэтому turnsLeft уменьшается.
eff.turnsLeft--;
}
// if (isNatureStrengthEffect) { // Отладочный лог
// console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect: ${eff.name}, justCast (after): ${eff.justCast}, turnsLeft (after): ${eff.turnsLeft}`);
// }
// --- Удаление закончившихся эффектов ---
if (eff.turnsLeft <= 0) {
// if (isNatureStrengthEffect) { // Отладочный лог
// console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect ${eff.name} REMOVED because turnsLeft is 0.`);
// }
effectsToRemoveIndexes.push(i);
if (addToLogCallback) {
addToLogCallback(`Эффект "${eff.name}" на ${ownerName} закончился.`, configToUse.LOG_TYPE_EFFECT);
}
}
}
for (let i = effectsToRemoveIndexes.length - 1; i >= 0; i--) {
effectsArray.splice(effectsToRemoveIndexes[i], 1);
}
}
/** Обрабатывает отсчет для отключенных (заглушенных) способностей игрока. */
function processDisabledAbilities(disabledAbilitiesArray, characterAbilities, characterName, addToLogCallback) {
if (!disabledAbilitiesArray || disabledAbilitiesArray.length === 0) return;
const stillDisabled = [];
disabledAbilitiesArray.forEach(dis => {
dis.turnsLeft--;
if (dis.turnsLeft > 0) {
stillDisabled.push(dis);
} else {
if (addToLogCallback) {
const ability = characterAbilities.find(ab => ab.id === dis.abilityId);
if (ability) addToLogCallback(`Способность ${characterName} "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO);
}
}
});
disabledAbilitiesArray.length = 0;
disabledAbilitiesArray.push(...stillDisabled);
}
/** Обрабатывает отсчет кулдаунов для способностей. */
function processPlayerAbilityCooldowns(cooldownsObject, abilitiesList, ownerName, addToLogCallback) {
if (!cooldownsObject || !abilitiesList) return;
for (const abilityId in cooldownsObject) {
if (cooldownsObject.hasOwnProperty(abilityId) && cooldownsObject[abilityId] > 0) {
cooldownsObject[abilityId]--;
if (cooldownsObject[abilityId] === 0) {
const ability = abilitiesList.find(ab => ab.id === abilityId);
if (ability && addToLogCallback) {
addToLogCallback(`Способность ${ownerName} "${ability.name}" снова готова!`, GAME_CONFIG.LOG_TYPE_INFO);
}
}
}
}
}
/** Обновляет статус 'isBlocking' на основе активных эффектов. */
function updateBlockingStatus(fighterState) {
if (!fighterState) return;
fighterState.isBlocking = fighterState.activeEffects.some(eff => eff.grantsBlock && eff.turnsLeft > 0);
}
/**
* Выбирает подходящую насмешку для Елены.
*/
function getElenaTaunt(trigger, context = {}, configToUse, gameDataForLogic = gameData, currentGameState) {
if (!currentGameState || !currentGameState.player || currentGameState.player.characterKey !== 'elena') {
return "(Молчание)";
}
const opponentKey = currentGameState.opponent.characterKey;
const tauntSystem = gameDataForLogic?.elenaTauntSystem;
const tauntBranch = opponentKey === 'balard' ? tauntSystem?.aiBalard : tauntSystem?.pvpAlmagest;
if (!tauntBranch) return "(Молчание)";
let potentialTaunts = [];
const opponentHpPerc = (currentGameState.opponent.currentHp / currentGameState.opponent.maxHp) * 100;
const isOpponentLowHpForDomination = opponentKey === 'balard' && opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT;
const isOpponentNearDefeat = opponentHpPerc < 20;
if (trigger === 'opponentNearDefeatCheck' && isOpponentNearDefeat && tauntBranch.onBattleState?.opponentNearDefeat) {
potentialTaunts = tauntBranch.onBattleState.opponentNearDefeat;
}
else if (trigger === 'opponentAction' && context.abilityId) {
const actionResponses = tauntBranch.onOpponentAction?.[context.abilityId];
if (actionResponses) {
if (typeof actionResponses === 'object' && !Array.isArray(actionResponses) && context.outcome && context.outcome in actionResponses) {
potentialTaunts = actionResponses[context.outcome];
} else if (Array.isArray(actionResponses)) {
potentialTaunts = actionResponses;
}
}
}
else if (trigger === 'opponentAttackBlocked' && tauntBranch.onOpponentAction?.attackBlocked) {
potentialTaunts = tauntBranch.onOpponentAction.attackBlocked;
}
else if (trigger === 'opponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) {
potentialTaunts = tauntBranch.onOpponentAction.attackHits;
}
else if (trigger === 'playerActionCast' && context.abilityId && tauntBranch.onPlayerCast?.[context.abilityId]) {
potentialTaunts = tauntBranch.onPlayerCast[context.abilityId];
}
else if (trigger === 'playerBasicAttack') {
if (isOpponentLowHpForDomination) {
const pools = tauntBranch.base?.dominating || {};
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
} else if (opponentKey === 'balard' && !isOpponentLowHpForDomination) {
potentialTaunts = tauntBranch.base?.mercifulAttack || [];
} else {
potentialTaunts = tauntBranch.base?.generalAttack || tauntBranch.base?.mercifulAttack || [];
}
}
else if (trigger === 'playerActionGeneral') {
if (isOpponentLowHpForDomination) {
const pools = tauntBranch.base?.dominating || {};
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
} else if (opponentKey === 'balard' && !isOpponentLowHpForDomination) {
potentialTaunts = tauntBranch.base?.mercifulCast || [];
} else {
potentialTaunts = tauntBranch.base?.generalCast || tauntBranch.base?.mercifulCast || [];
}
}
else if (trigger === 'battleStart') {
const startTaunts = (opponentKey === 'balard' ? tauntBranch.onBattleState?.startMerciful : tauntBranch.onBattleState?.start);
if (startTaunts) potentialTaunts = startTaunts;
}
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
if (opponentKey === 'balard') {
if (isOpponentLowHpForDomination) {
const pools = tauntBranch.base?.dominating || {};
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
} else {
potentialTaunts = [...(tauntBranch.base?.mercifulAttack || []), ...(tauntBranch.base?.mercifulCast || [])];
}
} else {
potentialTaunts = [...(tauntBranch.base?.generalAttack || []), ...(tauntBranch.base?.generalCast || [])];
}
}
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) return "(Молчание)";
return potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)] || "(Молчание)";
}
// --- Основные Игровые Функции ---
function performAttack(attackerState, defenderState, attackerBaseStats, defenderBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
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') {
const blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
} else {
let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
if (defenderState.characterKey === 'elena') {
const hitTaunt = getElenaTaunt('opponentAttackHit', {}, configToUse, gameDataForLogic, currentGameState);
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
}
if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
}
defenderState.currentHp = Math.max(0, defenderState.currentHp - damage);
}
function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
let tauntMessagePart = "";
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 += actualHeal;
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
} else {
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} уже имеет полное здоровье или эффект не дал лечения.`, 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 (targetState.characterKey === 'elena') {
const blockTaunt = getElenaTaunt('opponentAttackBlocked', {abilityId: ability.id} , configToUse, gameDataForLogic, currentGameState);
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}"! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
}
targetState.currentHp = Math.max(0, targetState.currentHp - damage);
if (addToLogCallback && !targetState.isBlocking) {
let hitMessage = `💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`;
if (targetState.characterKey === 'elena') {
const hitTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
}
addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
}
break;
case configToUse.ACTION_TYPE_BUFF:
if (!casterState.activeEffects.some(e => e.id === ability.id)) {
let effectDescription = ability.description;
if (typeof ability.descriptionFunction === 'function') {
const opponentRole = casterState.id === configToUse.PLAYER_ID ? configToUse.OPPONENT_ID : configToUse.PLAYER_ID;
const opponentCurrentState = currentGameState[opponentRole];
const opponentDataForDesc = opponentCurrentState ? gameDataForLogic[opponentCurrentState.characterKey + 'BaseStats'] : gameDataForLogic.playerBaseStats; // Фоллбэк
effectDescription = ability.descriptionFunction(configToUse, opponentDataForDesc);
}
casterState.activeEffects.push({
id: ability.id, name: ability.name, description: effectDescription,
type: ability.type, turnsLeft: ability.duration,
grantsBlock: !!ability.grantsBlock, justCast: !!ability.isDelayed
});
if (ability.grantsBlock) updateBlockingStatus(casterState);
} else {
if (addToLogCallback) addToLogCallback(`Эффект "${ability.name}" уже активен на ${casterBaseStats.name}!`, configToUse.LOG_TYPE_INFO);
}
break;
case configToUse.ACTION_TYPE_DISABLE:
tauntMessagePart = ""; // Сбрасываем перед каждым дизейблом
if (targetState.characterKey === 'elena') {
const disableTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
if (disableTaunt !== "(Молчание)") tauntMessagePart = ` (${disableTaunt})`;
}
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE && casterState.characterKey === 'elena') {
const effectId = 'fullSilenceByElena';
if (!targetState.activeEffects.some(e => e.id === effectId)) {
targetState.activeEffects.push({
id: effectId, name: ability.name, description: ability.description,
type: ability.type, turnsLeft: ability.effectDuration, power: ability.power, isFullSilence: true
});
if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}"! Способности ${targetBaseStats.name} заблокированы на ${ability.effectDuration} хода, и он(а) получает урон!`, configToUse.LOG_TYPE_EFFECT);
}
}
else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') {
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
const silenceOutcome = success ? 'success' : 'fail';
const specificSilenceTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : "";
if (success) {
const targetAbilities = gameDataForLogic.playerAbilities;
const availableAbilities = targetAbilities.filter(pa =>
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`)
);
if (availableAbilities.length > 0) {
const abilityToSilence = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
const turns = configToUse.SILENCE_DURATION;
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 });
const silenceEffectIdOnPlayer = `playerSilencedOn_${abilityToSilence.id}`;
targetState.activeEffects.push({
id: silenceEffectIdOnPlayer, name: `Безмолвие: ${abilityToSilence.name}`,
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
type: configToUse.ACTION_TYPE_DISABLE, turnsLeft: turns + 1
});
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" Елены заблокировано!${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);
}
}
else if (ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE && casterState.characterKey === 'almagest') {
const effectId = 'fullSilenceByAlmagest';
if (!targetState.activeEffects.some(e => e.id === effectId)) {
targetState.activeEffects.push({
id: effectId, name: ability.name, description: ability.description,
type: ability.type, turnsLeft: ability.effectDuration, power: ability.power, isFullSilence: true
});
if (addToLogCallback) addToLogCallback(`🧠 ${casterBaseStats.name} применяет "${ability.name}"! Способности ${targetBaseStats.name} заблокированы на ${ability.effectDuration} хода, и он(а) получает урон!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
}
}
break;
case configToUse.ACTION_TYPE_DEBUFF:
tauntMessagePart = "";
if (targetState.characterKey === 'elena') {
const debuffTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
if (debuffTaunt !== "(Молчание)") tauntMessagePart = ` (${debuffTaunt})`;
}
if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF) {
const effectIdForDebuff = 'effect_' + ability.id;
if (!targetState.activeEffects.some(e => e.id === effectIdForDebuff)) {
let effectDescription = ability.description;
if (typeof ability.descriptionFunction === 'function') {
effectDescription = ability.descriptionFunction(configToUse, targetBaseStats);
}
targetState.activeEffects.push({
id: effectIdForDebuff, name: ability.name, description: effectDescription,
type: configToUse.ACTION_TYPE_DEBUFF, sourceAbilityId: ability.id,
turnsLeft: ability.effectDuration, power: ability.power,
});
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
}
}
break;
case configToUse.ACTION_TYPE_DRAIN:
if (casterState.characterKey === 'balard') {
let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0;
tauntMessagePart = "";
if (targetState.characterKey === 'elena') {
const drainTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id }, configToUse, gameDataForLogic, currentGameState);
if (drainTaunt !== "(Молчание)") tauntMessagePart = ` (${drainTaunt})`;
}
if (ability.powerDamage > 0) {
let baseDamageDrain = ability.powerDamage;
if (targetState.isBlocking) {
baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION);
let blockDrainTaunt = "";
if (targetState.characterKey === 'elena') {
blockDrainTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
if (blockDrainTaunt !== "(Молчание)") blockDrainTaunt = ` (${blockDrainTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует часть урона от "${ability.name}"! Урон снижен до ${baseDamageDrain}.${blockDrainTaunt}`, configToUse.LOG_TYPE_BLOCK);
}
damageDealtDrain = Math.max(0, baseDamageDrain);
targetState.currentHp = Math.max(0, targetState.currentHp - damageDealtDrain);
}
const potentialDrain = ability.powerManaDrain;
const actualDrain = Math.min(potentialDrain, targetState.currentResource);
if (actualDrain > 0) {
targetState.currentResource -= actualDrain;
manaDrained = actualDrain;
const potentialHeal = Math.floor(manaDrained * ability.powerHealthGainFactor);
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
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:
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`);
}
}
function decideAiAction(currentGameState, gameDataForLogic = gameData, configToUse, addToLogCallback) {
const opponentState = currentGameState.opponent;
const playerState = currentGameState.player;
if (opponentState.characterKey !== 'balard') {
console.warn("[AI DEBUG] decideAiAction called for non-Balard opponent. This should not happen.");
return { actionType: 'pass', logMessage: { message: `${opponentState.name} (не AI) пропускает ход.`, type: configToUse.LOG_TYPE_INFO } };
}
const isBalardFullySilencedByElena = opponentState.activeEffects.some(
eff => eff.id === 'fullSilenceByElena' && eff.turnsLeft > 0
);
if (isBalardFullySilencedByElena) {
if (addToLogCallback) addToLogCallback(`😵 ${opponentState.name} под действием "Гипнотического взгляда"! Атакует в смятении.`, configToUse.LOG_TYPE_EFFECT);
return { actionType: 'attack' };
}
const availableActions = [];
const opponentAbilities = gameDataForLogic.opponentAbilities;
const healAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_HEAL);
if (healAbility && opponentState.currentResource >= healAbility.cost &&
healAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
availableActions.push({ weight: 80, type: 'ability', ability: healAbility, requiresSuccessCheck: true, successRate: healAbility.successRate });
}
const silenceAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE);
if (silenceAbility && opponentState.currentResource >= silenceAbility.cost &&
(opponentState.silenceCooldownTurns === undefined || opponentState.silenceCooldownTurns <= 0) &&
(!opponentState.abilityCooldowns || opponentState.abilityCooldowns[silenceAbility.id] === undefined || opponentState.abilityCooldowns[silenceAbility.id] <=0) &&
silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
const playerHpPercent = (playerState.currentHp / playerState.maxHp) * 100;
if (playerHpPercent > (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 40)) {
availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility, requiresSuccessCheck: true, successRate: configToUse.SILENCE_SUCCESS_RATE });
}
}
const drainAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN);
if (drainAbility && opponentState.currentResource >= drainAbility.cost &&
(opponentState.manaDrainCooldownTurns === undefined || opponentState.manaDrainCooldownTurns <= 0) &&
(!opponentState.abilityCooldowns || opponentState.abilityCooldowns[drainAbility.id] === undefined || opponentState.abilityCooldowns[drainAbility.id] <=0) &&
drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
availableActions.push({ weight: 50, type: 'ability', ability: drainAbility });
}
availableActions.push({ weight: 30, type: 'attack' });
if (availableActions.length === 0) {
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не может совершить действие.`, type: configToUse.LOG_TYPE_INFO } };
}
availableActions.sort((a, b) => b.weight - a.weight);
for (const action of availableActions) {
if (action.requiresSuccessCheck) {
if (Math.random() < action.successRate) {
return { actionType: action.type, ability: action.ability };
} else {
if (addToLogCallback) addToLogCallback(`💨 ${opponentState.name} пытается использовать "${action.ability.name}", но терпит неудачу!`, configToUse.LOG_TYPE_INFO);
continue;
}
} else {
return { actionType: action.type, ability: action.ability };
}
}
return { actionType: 'attack' };
}
function checkGameOverInternal(currentGameState, configToUse, gameDataForLogic = gameData) {
if (!currentGameState || currentGameState.isGameOver) return currentGameState ? currentGameState.isGameOver : true;
const playerDead = currentGameState.player.currentHp <= 0;
const opponentDead = currentGameState.opponent.currentHp <= 0;
return playerDead || opponentDead;
}
module.exports = {
processEffects,
processDisabledAbilities,
processPlayerAbilityCooldowns,
updateBlockingStatus,
getElenaTaunt,
performAttack,
applyAbilityEffect,
decideAiAction,
checkGameOverInternal
};