bc/server_modules/gameLogic.js
2025-05-15 16:20:25 +00:00

727 lines
58 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'); // Загружаем один раз на уровне модуля
// --- Вспомогательные Функции для gameLogic ---
// Вспомогательная функция для получения данных персонажа (baseStats и abilities)
// Нужна здесь, так как объект gameData сам по себе не имеет этих методов.
// Принимает gameDataForLogic как аргумент для гибкости, по умолчанию использует глобальный gameData.
function _getCharacterDataForLogic(key, gameDataForLogic = gameData) {
if (!key) return null;
switch (key) {
case 'elena': return { baseStats: gameDataForLogic.playerBaseStats, abilities: gameDataForLogic.playerAbilities };
case 'balard': return { baseStats: gameDataForLogic.opponentBaseStats, abilities: gameDataForLogic.opponentAbilities }; // Балард использует opponentAbilities
case 'almagest': return { baseStats: gameDataForLogic.almagestBaseStats, abilities: gameDataForLogic.almagestAbilities }; // Альмагест использует almagestAbilities
default: console.error(`_getCharacterDataForLogic: Неизвестный ключ персонажа "${key}"`); return null;
}
}
// Вспомогательная функция для получения только базовых статов персонажа
function _getCharacterBaseDataForLogic(key, gameDataForLogic = gameData) {
const charData = _getCharacterDataForLogic(key, gameDataForLogic);
return charData ? charData.baseStats : null;
}
// Вспомогательная функция для получения только способностей персонажа
function _getCharacterAbilitiesForLogic(key, gameDataForLogic = gameData) {
const charData = _getCharacterDataForLogic(key, gameDataForLogic);
return charData ? charData.abilities : null;
}
/**
* Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода.
* Длительность эффекта уменьшается на 1.
* Периодические эффекты (DoT, ресурсный дебафф и т.п.) срабатывают, если эффект не "justCast" в этом ходу.
* @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];
// --- Применяем эффект (DoT, сжигание ресурса и т.п.), если он не только что наложен в этом ходу ---
if (!eff.justCast) {
// Обработка урона от эффектов полного безмолвия (Гипнотический Взгляд, Раскол Разума)
// Эти эффекты наносят урон цели В КОНЦЕ ее хода
if (eff.isFullSilence && typeof eff.power === 'number' && eff.power > 0) {
const damage = eff.power;
// ИСПРАВЛЕНО: Округляем результат вычитания HP
ownerState.currentHp = Math.max(0, Math.round(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, Math.round(ownerState.currentResource - actualBurn));
if (addToLogCallback) addToLogCallback(`🔥 Эффект "${eff.name}" сжигает ${actualBurn} ${ownerBaseStats.resourceName} у ${ownerName}!`, configToUse.LOG_TYPE_EFFECT);
}
}
// Примечание: Отложенные эффекты (например, Сила Природы) применяют свою силу в gameInstance.processPlayerAction (после атаки), а не здесь.
}
// --- Уменьшаем длительность ---
eff.turnsLeft--;
eff.justCast = false; // Эффект больше не считается "just cast" после обработки этого хода
// --- Отмечаем для удаления, если длительность закончилась ---
if (eff.turnsLeft <= 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);
}
}
/**
* Обрабатывает отсчет для отключенных (заглушенных) способностей игрока в конце его хода.
* Длительность заглушения уменьшается на 1.
* @param {Array<object>} disabledAbilitiesArray - Массив объектов заглушенных способностей.
* @param {Array<object>} characterAbilities - Полный список способностей персонажа (для получения имени).
* @param {string} characterName - Имя персонажа (для лога).
* @param {function} addToLogCallback - Функция для добавления лога.
* @returns {void} - Модифицирует disabledAbilitiesArray напрямую.
*/
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);
// Проверка на заглушающий эффект тоже должна быть удалена из activeEffects в processEffects
// Здесь мы только обрабатываем список disabledAbilities, удаляя запись
if (ability) addToLogCallback(`Способность ${characterName} "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO);
}
}
});
// Очищаем исходный массив и добавляем только те, что еще активны
disabledAbilitiesArray.length = 0;
disabledAbilitiesArray.push(...stillDisabled);
}
/**
* Обрабатывает отсчет общих кулдаунов для способностей в конце хода.
* Длительность кулдауна уменьшается на 1.
* @param {object} cooldownsObject - Объект с кулдаунами способностей ({ abilityId: turnsLeft }).
* @param {Array<object>} abilitiesList - Полный список способностей персонажа (для получения имени).
* @param {string} ownerName - Имя персонажа (для лога).
* @param {function} addToLogCallback - Функция для добавления лога.
* @returns {void} - Модифицирует cooldownsObject напрямую.
*/
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' на основе активных эффектов.
* @param {object} fighterState - Состояние бойца.
* @returns {void} - Модифицирует fighterState.isBlocking.
*/
function updateBlockingStatus(fighterState) {
if (!fighterState) return;
// Боец считается блокирующим, если у него есть активный эффект, дающий блок (grantsBlock: true) с turnsLeft > 0
fighterState.isBlocking = fighterState.activeEffects.some(eff => eff.grantsBlock && eff.turnsLeft > 0);
}
/**
* Получает случайную насмешку из системы насмешек для определенного персонажа.
* Ищет фразу в gameData.tauntSystem[speakerCharacterKey][opponentCharacterKey][trigger][context].
* @param {string} speakerCharacterKey - Ключ персонажа, который произносит насмешку ('elena' или 'almagest' или 'balard').
* @param {string} trigger - Тип события, вызвавшего насмешку (например, 'selfCastAbility', 'onOpponentAction', 'battleStart', 'basicAttack', 'opponentNearDefeatCheck').
* @param {object} context - Дополнительный контекст (например, { abilityId: 'fireball' }, { outcome: 'success' }).
* @param {object} configToUse - Конфигурационный объект игры.
* @param {object} gameDataForLogic - Полный объект gameData.
* @param {object} currentGameState - Текущее состояние игры.
* @returns {string} Текст насмешки или "(Молчание)".
*/
function getRandomTaunt(speakerCharacterKey, trigger, context = {}, configToUse, gameDataForLogic = gameData, currentGameState) {
// Проверяем наличие системы насмешек для говорящего персонажа
const speakerTauntSystem = gameDataForLogic?.tauntSystem?.[speakerCharacterKey];
if (!speakerTauntSystem) return "(Молчание)"; // Нет насмешек для этого персонажа
// Определяем противника, чтобы выбрать соответствующую ветку насмешек
// Для этого нужно найти в gameState, кто из player/opponent имеет characterKey говорящего,
// и взять characterKey другого.
const speakerRole = currentGameState?.player?.characterKey === speakerCharacterKey ?
GAME_CONFIG.PLAYER_ID :
(currentGameState?.opponent?.characterKey === speakerCharacterKey ?
GAME_CONFIG.OPPONENT_ID : null);
if (speakerRole === null) {
console.warn(`getRandomTaunt: Speaker character key "${speakerCharacterKey}" not found in current game state roles.`);
return "(Молчание)";
}
const opponentRole = speakerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const opponentCharacterKey = currentGameState?.[opponentRole]?.characterKey;
const tauntBranch = speakerTauntSystem[opponentCharacterKey];
if (!tauntBranch) {
// console.warn(`getRandomTaunt: No taunt branch found for speaker "${speakerCharacterKey}" against opponent "${opponentCharacterKey}".`);
return "(Молчание)"; // Нет насмешек против этого оппонента
}
let potentialTaunts = [];
// Навигация по структуре tauntSystem в зависимости от триггера и контекста
if (trigger === 'battleStart') {
potentialTaunts = tauntBranch.onBattleState?.start;
}
else if (trigger === 'opponentNearDefeatCheck') { // Проверка на низкое HP противника для специальных фраз
const opponentState = currentGameState?.[opponentRole]; // Состояние противника для проверки HP
// Проверяем, что состояние оппонента существует и его HP ниже порога (например, 20%)
if (opponentState && opponentState.maxHp > 0 && opponentState.currentHp / opponentState.maxHp < 0.20) {
potentialTaunts = tauntBranch.onBattleState?.opponentNearDefeat;
}
}
else if (trigger === 'selfCastAbility' && context.abilityId) {
potentialTaunts = tauntBranch.selfCastAbility?.[context.abilityId];
}
else if (trigger === 'basicAttack' && tauntBranch.basicAttack) {
const opponentState = currentGameState?.[opponentRole]; // Состояние противника
// Специальная логика для базовой атаки Елены против Баларда (милосердие/доминирование)
if (speakerCharacterKey === 'elena' && opponentCharacterKey === 'balard' && opponentState) {
const opponentHpPerc = (opponentState.currentHp / opponentState.maxHp) * 100;
if (opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT) {
potentialTaunts = tauntBranch.basicAttack.dominating;
} else {
potentialTaunts = tauntBranch.basicAttack.merciful;
}
} else { // Общая логика для PvP или Елена/Балард вне порога
potentialTaunts = tauntBranch.basicAttack.general;
}
}
// Реакция на действие противника
else if (trigger === 'onOpponentAction' && 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]; // Например, onOpponentAction.silence.success
} else if (Array.isArray(actionResponses)) {
potentialTaunts = actionResponses; // Прямой массив фраз для способности
}
}
}
// Реакция на попадание/блок атаки противника
// Примечание: Эти триггеры срабатывают, когда по ГОВОРЯЩЕМУ попала атака или он ее заблокировал.
// Вызываются из performAttack, где известно, кто атакует и кто защищается.
else if (trigger === 'onOpponentAttackBlocked' && tauntBranch.onOpponentAction?.attackBlocked) {
potentialTaunts = tauntBranch.onOpponentAction.attackBlocked;
}
else if (trigger === 'onOpponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) {
potentialTaunts = tauntBranch.onOpponentAction.attackHits;
}
// Если по прямому триггеру не найдено, возвращаем "(Молчание)".
// Можно добавить фоллбэк на общие фразы, если требуется более разговорчивый персонаж.
// Например: if ((!potentialTaunts || potentialTaunts.length === 0) && tauntBranch.basicAttack?.general) { potentialTaunts = tauntBranch.basicAttack.general; }
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
return "(Молчание)"; // Возвращаем молчание, если ничего не найдено
}
// Возвращаем случайную фразу из найденного массива
const selectedTaunt = potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)];
return selectedTaunt || "(Молчание)"; // Фоллбэк на "(Молчание)" если массив был пуст после всех проверок
}
// --- Основные Игровые Функции ---
/**
* Обрабатывает базовую атаку одного бойца по другому.
* @param {object} attackerState - Состояние атакующего бойца.
* @param {object} defenderState - Состояние защищающегося бойца.
* @param {object} attackerBaseStats - Базовые статы атакующего.
* @param {object} defenderBaseStats - Базовые статы защищающегося.
* @param {object} currentGameState - Текущее состояние игры (для насмешек).
* @param {function} addToLogCallback - Функция для добавления лога.
* @param {object} configToUse - Конфигурация игры.
* @param {object} gameDataForLogic - Данные игры (для насмешек).
*/
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' || defenderState.characterKey === 'almagest') {
// getRandomTaunt принимает speaker (защищающийся), trigger, context, config, gameData, gameState
const blockTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
} else {
let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
// Проверка на насмешку ОТ защищающегося (Елены или Альмагест) при попадании атаки
if (defenderState.characterKey === 'elena' || defenderState.characterKey === 'almagest') {
// getRandomTaunt принимает speaker (защищающийся), trigger, context, config, gameData, gameState
const hitTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackHit', {}, configToUse, gameDataForLogic, currentGameState);
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
}
if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
}
// Применяем урон, убеждаемся, что HP не ниже нуля
// ИСПРАВЛЕНО: Округляем результат вычитания HP
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
}
/**
* Применяет эффект способности.
* @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} gameDataForLogic - Данные игры (для насмешек).
*/
function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
let tauntMessagePart = ""; // Переменная для насмешки, если она связана с результатом эффекта или реакцией цели
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под способность противника
if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
// Триггер 'onOpponentAction' с abilityId противника
const reactionTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id }, configToUse, gameDataForLogic, currentGameState);
if (reactionTaunt !== "(Молчание)") tauntMessagePart = ` (${reactionTaunt})`;
} else {
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) {
// ИСПРАВЛЕНО: Округляем результат прибавления HP
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 (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
// const blockTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAttackBlocked', {abilityId: ability.id} , configToUse, gameDataForLogic, currentGameState);
// if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
// }
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
}
// Применяем урон, убеждаемся, что HP не ниже нуля
// ИСПРАВЛЕНО: Округляем результат вычитания HP
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
if (addToLogCallback && !targetState.isBlocking) {
let hitMessage = `💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!${tauntMessagePart}`;
// Проверка на насмешку ОТ цели (Елены или Альмагест), если по ней попала способность - перенесено наверх
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
// const hitTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
// if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
// }
addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
}
break;
case configToUse.ACTION_TYPE_BUFF:
// Если бафф уже активен, не применяем его повторно (эта проверка уже есть в gameInstance)
// Проверка на .some здесь опциональна, т.к. вызывающий код должен гарантировать уникальность
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?.characterKey ? _getCharacterBaseDataForLogic(opponentCurrentState.characterKey, gameDataForLogic) : null; // ИСПОЛЬЗУЕМ _getCharacterBaseDataForLogic
effectDescription = ability.descriptionFunction(configToUse, opponentDataForDesc);
}
// isDelayed: true используется для эффектов, которые срабатывают ПОСЛЕ следующего действия (например, Сила Природы).
// duration: исходная длительность из данных, turnsLeft: сколько ходов осталось (включая текущий, если !justCast)
casterState.activeEffects.push({
id: ability.id, name: ability.name, description: effectDescription,
type: ability.type, duration: ability.duration, // Сохраняем исходную длительность для отображения в UI или логики
turnsLeft: ability.duration, // Длительность жизни эффекта в ходах владельца
grantsBlock: !!ability.grantsBlock,
isDelayed: !!ability.isDelayed, // Флаг, что эффект отложенный (срабатывает после действия)
justCast: true // Флаг, что эффект только что наложен (для логики processEffects)
});
if (ability.grantsBlock) updateBlockingStatus(casterState); // Обновляем статус блока кастера, если бафф его дает
// Насмешки при применении баффа (selfCastAbility) добавляются в GameInstance перед вызовом applyAbilityEffect
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} накладывает эффект "${ability.name}"!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
} else {
// Сообщение "уже активен" отправляется из gameInstance перед вызовом applyAbilityEffect
}
break;
case configToUse.ACTION_TYPE_DISABLE: // Безмолвие, Стан и т.п.
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под дизейбл противника - перенесено наверх
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
// const disableTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
// if (disableTaunt !== "(Молчание)") tauntMessagePart = ` (${disableTaunt})`;
// }
// Гипнотический взгляд Елены / Раскол Разума Альмагест (полное безмолвие)
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
const effectId = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
// Не накладываем повторно, если эффект уже есть на цели
if (!targetState.activeEffects.some(e => e.id === effectId)) {
targetState.activeEffects.push({
id: effectId, 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}! Способности ${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;
const silenceOutcome = success ? 'success' : 'fail';
// Реакция цели (Елены) на успех/провал безмолвия Баларда - перенесено наверх, но с context.outcome
// if (targetState.characterKey === 'elena') { // Балард применяет это только на Елену
// const specificSilenceTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
// tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : "";
// } else {
// tauntMessagePart = ""; // Другие персонажи не реагируют на Безмолвие Баларда
// }
// Нужно получить насмешку с outcome здесь, так как она зависит от результата броска шанса
// Временно сохраняем общую насмешку и получаем специфичную
let specificSilenceTaunt = "(Молчание)";
if (targetState.characterKey === 'elena') { // Балард применяет это только на Елену
specificSilenceTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
}
tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : tauntMessagePart; // Используем специфичную, если найдена, иначе общую (хотя общая для этого абилки вряд ли есть)
if (success) {
const targetAbilities = _getCharacterAbilitiesForLogic(targetState.characterKey, gameDataForLogic); // Глушим абилки цели
// Фильтруем способности, которые еще не заглушены этим типом безмолвия
const availableAbilities = targetAbilities.filter(pa =>
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`) // Проверка на эффект заглушения для UI/ProcessEffects
);
if (availableAbilities.length > 0) {
const abilityToSilence = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
const turns = configToUse.SILENCE_DURATION; // Длительность из конфига (в ходах цели)
// Добавляем запись о заглушенной способности в disabledAbilities цели
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); // +1, т.к. длительность уменьшается в конце хода цели
// Добавляем эффект заглушения в activeEffects цели (для UI и ProcessEffects)
const silenceEffectIdOnPlayer = `playerSilencedOn_${abilityToSilence.id}`;
targetState.activeEffects.push({
id: silenceEffectIdOnPlayer, name: `Безмолвие: ${abilityToSilence.name}`,
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
type: configToUse.ACTION_TYPE_DISABLE, sourceAbilityId: ability.id, // Добавлено sourceAbilityId
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: // Ослабления, DoT и т.п.
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под дебафф противника - перенесено наверх
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
// const debuffTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {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; // Уникальный 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,
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;
// tauntMessagePart уже получена в начале функции
// Сначала урон от способности
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' || targetState.characterKey === 'almagest') {
blockDrainTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
if (blockDrainTaunt !== "(Молчание)") blockDrainTaunt = ` (${blockDrainTaunt})`;
}
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует часть урона от "${ability.name}" от ${casterBaseStats.name}! Урон снижен до ${baseDamageDrain}.${blockDrainTaunt}`, configToUse.LOG_TYPE_BLOCK);
}
damageDealtDrain = Math.max(0, baseDamageDrain);
// ИСПРАВЛЕНО: Округляем результат вычитания HP
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);
// ИСПРАВЛЕНО: Округляем результат прибавления HP
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 { // Ни урона, ни вытягивания ресурса
// ИСПРАВЛЕНО: targetBaseStats.resourceName -> targetState.resourceName (или defenderBaseStats.resourceName, если он передается)
logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; // Оставляем targetBaseStats.resourceName, т.к. он точнее для лога
// Если урон был 0, и ресурса нет, можно уточнить лог
if(damageDealtDrain === 0 && potentialDrain > 0) logMsgDrain += ` Урон не прошел или равен нулю, ресурс не похищен.`;
}
logMsgDrain += tauntMessagePart; // Добавляем насмешку цели, если была
if (addToLogCallback) addToLogCallback(logMsgDrain, manaDrained > 0 || damageDealtDrain > 0 ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
} else {
console.warn(`applyAbilityEffect: Drain type ability ${ability?.name} used by non-Balard character ${casterState.characterKey}`);
}
break;
default:
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`);
}
}
/**
* Логика принятия решения для AI (Балард).
* @param {object} currentGameState - Текущее состояние игры.
* @param {object} gameDataForLogic - Данные игры.
* @param {object} configToUse - Конфигурация игры.
* @param {function} addToLogCallback - Функция для добавления лога.
* @returns {object} Объект с действием AI ({ actionType: 'attack' | 'ability', ability?: object }).
*/
function decideAiAction(currentGameState, gameDataForLogic = gameData, configToUse, addToLogCallback) {
const opponentState = currentGameState.opponent; // AI Балард всегда в слоте opponent
const playerState = currentGameState.player; // Игрок всегда в слоте player (в AI режиме)
// Убеждаемся, что это AI Балард
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 isBalardFullySilenced = opponentState.activeEffects.some(
eff => eff.isFullSilence && eff.turnsLeft > 0
);
if (isBalardFullySilenced) {
// AI под полным безмолвием просто атакует
// Лог о безмолвии и атаке в смятении добавляется в processAiTurn перед вызовом performAttack.
// decideAiAction просто возвращает действие.
return { actionType: 'attack' };
}
const availableActions = [];
const opponentAbilities = gameDataForLogic.opponentAbilities; // Способности Баларда
// Проверяем доступность способностей AI и добавляем их в список возможных действий с весом
// Вес определяет приоритет: выше вес -> выше шанс выбора (после сортировки)
const healAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_HEAL);
if (healAbility && opponentState.currentResource >= healAbility.cost &&
(opponentState.abilityCooldowns?.[healAbility.id] || 0) <= 0 && // Проверка общего КД (хотя у Баларда могут быть только спец. КД)
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?.[silenceAbility.id] || 0) <= 0 && // Проверка общего КД
silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
const playerHpPercent = (playerState.currentHp / playerState.maxHp) * 100;
// Балард предпочитает безмолвие, если HP Елены не слишком низкое (позволяет ей лечиться, чтобы игра длилась дольше)
if (playerHpPercent > (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 60)) { // Используем порог для текстов Елены как пример
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?.[drainAbility.id] || 0) <= 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.type === 'ability') {
// Если способность требует проверки успеха (например, Безмолвие Баларда)
if (action.requiresSuccessCheck) {
if (Math.random() < action.successRate) {
// Успех, добавляем лог о попытке (чтобы было видно, что AI пытался)
if (addToLogCallback) addToLogCallback(`${opponentState.name} пытается использовать "${action.ability.name}"...`, configToUse.LOG_TYPE_INFO);
return { actionType: action.type, ability: action.ability }; // Успех, выбираем эту способность
} else {
// Провал, добавляем лог о провале и переходим к следующему возможному действию в цикле
if (addToLogCallback) addToLogCallback(`💨 Попытка ${opponentState.name} использовать "${action.ability.name}" провалилась!`, configToUse.LOG_TYPE_INFO);
continue; // Пробуем следующее действие в списке
}
} else {
// Нет проверки успеха, добавляем лог о попытке и выбираем способность
if (addToLogCallback) addToLogCallback(`${opponentState.name} использует "${action.ability.name}"...`, configToUse.LOG_TYPE_INFO);
return { actionType: action.type, ability: action.ability };
}
} else if (action.type === 'attack') {
// Атака - всегда возможна (если нет полного безмолвия, проверено выше)
if (addToLogCallback) addToLogCallback(`🦶 ${opponentState.name} готовится к атаке...`, configToUse.LOG_TYPE_INFO);
return { actionType: 'attack' };
}
// 'pass' не должен быть в доступных действиях, если атака всегда доступна
}
// Если все попытки выбрать способность или атаку провалились (очень маловероятно, если атака всегда в списке), пропуск хода
console.warn("[AI DEBUG] AI failed to select any action. Defaulting to pass.");
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не смог выбрать подходящее действие. Пропускает ход.`, type: configToUse.LOG_TYPE_INFO } };
}
/**
* Внутренняя проверка условий конца игры (основано на HP).
* @param {object} currentGameState - Текущее состояние игры.
* @param {object} configToUse - Конфигурация игры.
* @param {object} gameDataForLogic - Данные игры.
* @returns {boolean} true, если игра окончена, иначе false.
*/
function checkGameOverInternal(currentGameState, configToUse, gameDataForLogic = gameData) {
// Проверка на конец игры происходит только если gameState существует и игра еще не помечена как оконченная
if (!currentGameState || currentGameState.isGameOver) return currentGameState ? currentGameState.isGameOver : true;
// Убеждаемся, что оба бойца определены в gameState и не являются плейсхолдерами
// Проверка maxHp > 0 в gameState.opponent гарантирует, что оппонент не плейсхолдер
if (!currentGameState.player || !currentGameState.opponent || currentGameState.opponent.maxHp <= 0) {
// Если один из бойцов не готов (например, PvP игра ожидает второго игрока), игра не может закончиться по HP
return false;
}
const playerDead = currentGameState.player.currentHp <= 0;
const opponentDead = currentGameState.opponent.currentHp <= 0;
// Игра окончена, если один или оба бойца мертвы
return playerDead || opponentDead;
}
// Экспортируем все функции, которые используются в других модулях
module.exports = {
processEffects,
processDisabledAbilities,
processPlayerAbilityCooldowns,
updateBlockingStatus,
getRandomTaunt, // Экспортируем переименованную функцию
performAttack,
applyAbilityEffect,
decideAiAction,
checkGameOverInternal
};