bc/server_modules/gameLogic.js
2025-05-09 12:11:07 +00:00

505 lines
31 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'); // <--- ДОБАВЛЕНО
// Этот файл содержит основную логику игры, адаптированную для сервера.
// ...
// --- Вспомогательные Функции ---
/**
* Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода.
* @param {Array} effectsArray - Массив активных эффектов бойца.
* @param {Object} ownerState - Состояние бойца (currentHp, currentResource и т.д.).
* @param {Object} ownerBaseStats - Базовые статы бойца.
* @param {String} ownerId - ID бойца ('player' или 'opponent').
* @param {Object} currentGameState - Полное состояние игры (для доступа к противнику, если нужно).
* @param {Function} addToLogCallback - Функция для добавления сообщений в лог игры.
* @param {Object} passed_GAME_CONFIG - Конфигурационный объект игры (переименовал, чтобы не конфликтовать с верхним GAME_CONFIG).
* @returns {void} - Модифицирует effectsArray и ownerState напрямую.
*/
// В processEffects уже передается GAME_CONFIG, так что можно использовать его (или тот, что require выше)
function processEffects(effectsArray, ownerState, ownerBaseStats, ownerId, currentGameState, addToLogCallback, passed_GAME_CONFIG) {
if (!effectsArray) return;
const ownerName = ownerBaseStats.name;
const configToUse = passed_GAME_CONFIG || GAME_CONFIG; // Используем переданный, если есть, иначе из require
let effectsToRemoveIndexes = [];
for (let i = 0; i < effectsArray.length; i++) {
const eff = effectsArray[i];
if (eff.id === 'fullSilenceByElena' && 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);
// checkGameOverInternal теперь тоже должен получать configToUse и gameData
if (checkGameOverInternal(currentGameState, configToUse, require('./data'))) { /* ... */ }
}
if (eff.id === ('effect_' + configToUse.ABILITY_ID_SEAL_OF_WEAKNESS) && ownerId === configToUse.OPPONENT_ID) {
const resourceToBurn = eff.power;
if (resourceToBurn > 0 && 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 {
eff.turnsLeft--;
}
if (eff.turnsLeft <= 0) {
effectsToRemoveIndexes.push(i);
let effectEndMessage = `Эффект "${eff.name}" на ${ownerName} закончился.`;
if (addToLogCallback) addToLogCallback(effectEndMessage, configToUse.LOG_TYPE_EFFECT);
if (eff.id === configToUse.ABILITY_ID_DEFENSE_AURA && eff.grantsBlock) {
ownerState.isBlocking = false;
}
}
}
for (let i = effectsToRemoveIndexes.length - 1; i >= 0; i--) {
effectsArray.splice(effectsToRemoveIndexes[i], 1);
}
}
/** Обрабатывает отсчет для отключенных (заглушенных) способностей Елены. */
function processDisabledAbilities(disabledAbilitiesArray, addToLogCallback /*, configToUse - если нужен */) {
if (!disabledAbilitiesArray || disabledAbilitiesArray.length === 0) return;
const localGameData = require('./data'); // Загружаем gameData здесь, если он нужен локально
const stillDisabled = [];
disabledAbilitiesArray.forEach(dis => {
dis.turnsLeft--;
if (dis.turnsLeft > 0) {
stillDisabled.push(dis);
} else {
if (addToLogCallback) {
const ability = localGameData.playerAbilities.find(ab => ab.id === dis.abilityId);
if (ability) addToLogCallback(`Способность Елены "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO); // Используем GAME_CONFIG из require
}
}
});
disabledAbilitiesArray.length = 0;
disabledAbilitiesArray.push(...stillDisabled);
}
/** Обрабатывает отсчет кулдаунов для способностей игрока (или Баларда-игрока). */
function processPlayerAbilityCooldowns(cooldownsObject, addToLogCallback, ownerName = "Елены", abilitiesList) {
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) {
// GAME_CONFIG.LOG_TYPE_INFO теперь доступен из require('./config') вверху файла
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);
}
/** Выбирает подходящую насмешку для Елены. */
// GAME_CONFIG и gameData передаются, так что используем их
function getElenaTaunt(trigger, context = {}, passed_GAME_CONFIG, passed_gameData, currentGameState) {
const configToUse = passed_GAME_CONFIG || GAME_CONFIG;
const gameDataToUse = passed_gameData || require('./data'); // Фоллбэк, если не передан
const tauntSystem = gameDataToUse?.elenaTauntSystem;
if (!tauntSystem || !currentGameState) {
return "(Молчание)";
}
let potentialTaunts = [];
const opponentHpPerc = (currentGameState.opponent.currentHp / currentGameState.opponent.maxHp) * 100;
const isOpponentLowHpForDomination = opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT;
const isOpponentNearDefeat = opponentHpPerc < 20; // Можно вынести в configToUse
if (trigger === 'opponentNearDefeatCheck' && isOpponentNearDefeat && tauntSystem.onBattleState?.opponentNearDefeat) {
potentialTaunts = tauntSystem.onBattleState.opponentNearDefeat;
}
else if (trigger === 'opponentAction' && context.abilityId) {
const actionResponses = tauntSystem.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' && tauntSystem.onOpponentAction?.attackBlocked) {
potentialTaunts = tauntSystem.onOpponentAction.attackBlocked;
}
else if (trigger === 'opponentAttackHit' && tauntSystem.onOpponentAction?.attackHits) {
potentialTaunts = tauntSystem.onOpponentAction.attackHits;
}
else if (trigger === 'playerActionCast' && context.abilityId && tauntSystem.onPlayerCast?.[context.abilityId]) {
potentialTaunts = tauntSystem.onPlayerCast[context.abilityId];
}
else if (trigger === 'playerBasicAttack') {
if (isOpponentLowHpForDomination) {
const pools = tauntSystem.base?.dominating || {};
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
} else {
potentialTaunts = tauntSystem.base?.mercifulAttack || [];
}
}
else if (trigger === 'playerActionGeneral') {
if (isOpponentLowHpForDomination) {
const pools = tauntSystem.base?.dominating || {};
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
} else {
potentialTaunts = tauntSystem.base?.mercifulCast || [];
}
}
else if (trigger === 'battleStart' && tauntSystem.onBattleState?.startMerciful) {
potentialTaunts = tauntSystem.onBattleState.startMerciful;
}
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
if (isOpponentLowHpForDomination) {
const pools = tauntSystem.base?.dominating || {};
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
} else {
potentialTaunts = [...(tauntSystem.base?.mercifulAttack || []), ...(tauntSystem.base?.mercifulCast || [])];
}
}
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
return "(Молчание)";
}
return potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)] || "(Молчание)";
}
// --- Основные Игровые Функции ---
// performAttack, applyAbilityEffect, decideAiAction уже получают GAME_CONFIG и gameData как параметры
// поэтому внутри них можно использовать переданные значения.
/**
* Выполняет базовую атаку.
* @param {Object} attackerState Состояние атакующего.
* @param {Object} defenderState Состояние защищающегося.
* @param {Object} attackerBaseStats Базовые статы атакующего.
* @param {Object} defenderBaseStats Базовые статы защищающегося.
* @param {Object} currentGameState Полное состояние игры.
* @param {Function} addToLogCallback Функция для логгирования.
* @param {Object} passed_GAME_CONFIG Конфиг игры.
*/
function performAttack(attackerState, defenderState, attackerBaseStats, defenderBaseStats, currentGameState, addToLogCallback, passed_GAME_CONFIG) {
const configToUse = passed_GAME_CONFIG || GAME_CONFIG; // Используем переданный или из require
const localGameData = require('./data'); // Для доступа к gameData внутри, если нужно для таунтов
let damage = Math.floor(attackerBaseStats.attackPower * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
let wasBlocked = false;
if (defenderState.isBlocking) {
wasBlocked = true;
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
let blockTaunt = "";
if (attackerState.id === configToUse.OPPONENT_ID && defenderState.id === configToUse.PLAYER_ID) {
blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, localGameData, currentGameState);
if (blockTaunt === "(Молчание)") blockTaunt = ""; else blockTaunt = ` (${blockTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку!${blockTaunt} Урон снижен до: ${damage}.`, configToUse.LOG_TYPE_BLOCK);
} else {
let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
if (attackerState.id === configToUse.OPPONENT_ID && defenderState.id === configToUse.PLAYER_ID) {
let hitTaunt = getElenaTaunt('opponentAttackHit', {}, configToUse, localGameData, currentGameState);
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
}
if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
}
defenderState.currentHp = Math.max(0, defenderState.currentHp - damage);
// checkGameOverInternal(currentGameState, configToUse, localGameData);
}
/**
* Применяет эффекты способности.
* @param {Object} ability - Объект способности из gameData.
* @param {Object} casterState - Состояние того, кто кастует.
* @param {Object} targetState - Состояние цели.
* @param {Object} casterBaseStats - Базовые статы кастера.
* @param {Object} targetBaseStats - Базовые статы цели.
* @param {Object} currentGameState - Полное состояние игры.
* @param {Function} addToLogCallback - Функция для логгирования.
* @param {Object} passed_GAME_CONFIG - Конфиг игры.
*/
function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, passed_GAME_CONFIG) {
const configToUse = passed_GAME_CONFIG || GAME_CONFIG;
const localGameData = require('./data'); // Для доступа к playerAbilities и т.д.
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) {
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
let blockTaunt = "";
if (casterState.id === configToUse.PLAYER_ID && targetState.id === configToUse.OPPONENT_ID) {
// No specific taunt here
} else if (casterState.id === configToUse.OPPONENT_ID && targetState.id === configToUse.PLAYER_ID) {
blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, localGameData, currentGameState);
if (blockTaunt === "(Молчание)") blockTaunt = ""; else blockTaunt = ` (${blockTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}"!${blockTaunt} Урон снижен до ${damage}.`, configToUse.LOG_TYPE_BLOCK);
}
targetState.currentHp = Math.max(0, targetState.currentHp - damage);
if (addToLogCallback && !targetState.isBlocking) addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, 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') {
effectDescription = ability.descriptionFunction(configToUse, localGameData.opponentBaseStats);
}
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:
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE) {
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) {
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
const silenceOutcome = success ? 'success' : 'fail';
const silenceTaunt = getElenaTaunt('opponentAction', { abilityId: configToUse.ABILITY_ID_BALARD_SILENCE, outcome: silenceOutcome }, configToUse, localGameData, currentGameState);
if (success) {
const availableAbilities = localGameData.playerAbilities.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}" Елены заблокировано! ${silenceTaunt}`, configToUse.LOG_TYPE_EFFECT);
} else {
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить! ${silenceTaunt}`, configToUse.LOG_TYPE_INFO);
}
} else {
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась! ${silenceTaunt}`, configToUse.LOG_TYPE_INFO);
}
}
break;
case configToUse.ACTION_TYPE_DEBUFF:
if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS) {
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,
});
}
}
break;
case 'drain': // Похищение Света Балардом
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);
const blockDrainTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, localGameData, currentGameState);
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;
}
const drainTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id }, configToUse, localGameData, currentGameState);
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 (drainTaunt !== "(Молчание)") logMsgDrain += ` (${drainTaunt})`;
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}"`);
}
// checkGameOverInternal(currentGameState, configToUse, localGameData);
}
/**
* Логика принятия решения для AI (Баларда).
* @param {Object} currentGameState - Текущее состояние игры.
* @param {Object} passed_gameData - Данные игры (статы, абилки).
* @param {Object} passed_GAME_CONFIG - Конфиг игры.
* @param {Function} addToLogCallback - Для логгирования действий AI.
* @returns {Object} - Объект с решением AI.
*/
function decideAiAction(currentGameState, passed_gameData, passed_GAME_CONFIG, addToLogCallback) {
const configToUse = passed_GAME_CONFIG || GAME_CONFIG;
const gameDataToUse = passed_gameData || require('./data');
const opponentState = currentGameState.opponent;
const playerState = currentGameState.player;
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 healAbility = gameDataToUse.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 });
}
const silenceAbility = gameDataToUse.opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE);
if (silenceAbility && opponentState.currentResource >= silenceAbility.cost &&
opponentState.silenceCooldownTurns <= 0 && // Check specific CD flag
(!opponentState.abilityCooldowns || opponentState.abilityCooldowns[silenceAbility.id] === undefined || opponentState.abilityCooldowns[silenceAbility.id] <=0) && // Also check general CD if used
silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
const isPlayerLowHpForSilence = (playerState.currentHp / playerState.maxHp) * 100 < (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 40);
const isPlayerAlreadySilenced = playerState.disabledAbilities.length > 0 || playerState.activeEffects.some(e => e.id.startsWith('playerSilencedOn_'));
if (!isPlayerLowHpForSilence && !isPlayerAlreadySilenced) {
availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility });
}
}
const drainAbility = gameDataToUse.opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN);
if (drainAbility && opponentState.currentResource >= drainAbility.cost &&
opponentState.manaDrainCooldownTurns <= 0 && // Check specific CD flag
(!opponentState.abilityCooldowns || opponentState.abilityCooldowns[drainAbility.id] === undefined || opponentState.abilityCooldowns[drainAbility.id] <=0) && // Also check general CD if used
drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
availableActions.push({ weight: 50, type: 'ability', ability: drainAbility });
}
availableActions.push({ weight: 30, type: 'attack' });
if (availableActions.length > 0) {
availableActions.sort((a, b) => b.weight - a.weight);
const chosenAction = availableActions[0];
if (chosenAction.type === 'ability' && chosenAction.ability.id === configToUse.ABILITY_ID_BALARD_HEAL) {
if (Math.random() < chosenAction.ability.successRate) {
return { actionType: 'ability', ability: chosenAction.ability };
} else {
if (addToLogCallback) addToLogCallback(`💨 ${opponentState.name} пытается использовать "${chosenAction.ability.name}", но терпит неудачу!`, configToUse.LOG_TYPE_INFO);
const nextAction = availableActions.find(act => act.ability?.id !== configToUse.ABILITY_ID_BALARD_HEAL && act.type !== 'pass');
if (nextAction) return { actionType: nextAction.type, ability: nextAction.ability };
return { actionType: 'attack' };
}
}
return { actionType: chosenAction.type, ability: chosenAction.ability };
}
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не может совершить действие.`, type: configToUse.LOG_TYPE_INFO } };
}
/**
* Внутренняя функция для проверки конца игры без запуска полного цикла событий gameOver.
* @param {Object} currentGameState
* @param {Object} passed_GAME_CONFIG
* @param {Object} passed_gameData - Не используется здесь, но для консистентности
* @returns {boolean} true, если игра окончена.
*/
function checkGameOverInternal(currentGameState, passed_GAME_CONFIG, passed_gameData) {
const configToUse = passed_GAME_CONFIG || GAME_CONFIG;
// const gameDataToUse = passed_gameData || require('./data'); // Не используется здесь
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
};