510 lines
32 KiB
JavaScript
510 lines
32 KiB
JavaScript
// /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
|
||
}; |