Выправление эффекта сила природы.
This commit is contained in:
parent
fb36c3d2e1
commit
12d85b8385
@ -12,9 +12,9 @@ class GameInstance {
|
||||
this.mode = mode;
|
||||
this.gameManager = gameManager;
|
||||
|
||||
this.players = {}; // { socketId: { id (role), socket, chosenCharacterKey, identifier, isTemporarilyDisconnected }}
|
||||
this.playerSockets = {}; // { roleId: socket } -> для быстрого доступа к сокету по роли
|
||||
this.playerCount = 0; // Только активные, не isTemporarilyDisconnected
|
||||
this.players = {};
|
||||
this.playerSockets = {};
|
||||
this.playerCount = 0;
|
||||
|
||||
this.gameState = null;
|
||||
this.aiOpponent = (mode === 'ai');
|
||||
@ -24,8 +24,8 @@ class GameInstance {
|
||||
this.opponentCharacterKey = null;
|
||||
this.ownerIdentifier = null;
|
||||
|
||||
this.reconnectTimers = {}; // { roleId: { timerId, updateIntervalId, startTimeMs, durationMs } }
|
||||
this.pausedTurnState = null; // { remainingTime: number, forPlayerRoleIsPlayer: boolean, isAiCurrentlyMoving: boolean }
|
||||
this.reconnectTimers = {};
|
||||
this.pausedTurnState = null;
|
||||
|
||||
this.turnTimer = new TurnTimer(
|
||||
GAME_CONFIG.TURN_DURATION_MS,
|
||||
@ -49,11 +49,9 @@ class GameInstance {
|
||||
|
||||
_sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) {
|
||||
if (!characterState || !characterState.characterKey) {
|
||||
// console.warn(`[Taunt ${this.id}] _sayTaunt: Caller character or characterKey is missing. Speaker: ${characterState?.name}, Trigger: ${triggerType}`);
|
||||
return;
|
||||
}
|
||||
if (!opponentCharacterKey) {
|
||||
// console.warn(`[Taunt ${this.id}] _sayTaunt: Opponent characterKey is missing for ${characterState.name}. Trigger: ${triggerType}`);
|
||||
return;
|
||||
}
|
||||
if (!gameLogic.getRandomTaunt) {
|
||||
@ -61,7 +59,6 @@ class GameInstance {
|
||||
return;
|
||||
}
|
||||
if (!this.gameState) {
|
||||
// console.warn(`[Taunt ${this.id}] _sayTaunt: this.gameState is null. Speaker: ${characterState.name}, Trigger: ${triggerType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -87,7 +84,6 @@ class GameInstance {
|
||||
|
||||
const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey);
|
||||
if (!opponentFullData) {
|
||||
// console.warn(`[Taunt ${this.id}] _sayTaunt: Could not get full data for opponent ${opponentCharacterKey} when ${characterState.name} tries to taunt.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -106,6 +102,7 @@ class GameInstance {
|
||||
}
|
||||
|
||||
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
|
||||
// ... (Код addPlayer без изменений из предыдущего вашего файла) ...
|
||||
console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`);
|
||||
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
|
||||
|
||||
@ -115,30 +112,29 @@ class GameInstance {
|
||||
console.warn(`[GameInstance ${this.id}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`);
|
||||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||
this.gameManager._cleanupGame(this.id, `rejoin_attempt_to_finished_game_${identifier}`);
|
||||
return false; // Изменили возврат на boolean, как ожидает GameManager
|
||||
return false;
|
||||
}
|
||||
if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
|
||||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||
}
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
|
||||
return false; // Изменили возврат
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Object.keys(this.players).length >= 2 && this.playerCount >=2) {
|
||||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
||||
return false; // Изменили возврат
|
||||
return false;
|
||||
}
|
||||
|
||||
let assignedPlayerId;
|
||||
let actualCharacterKey = chosenCharacterKey || 'elena';
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { // Проверяем, занят ли слот игрока-человека
|
||||
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||||
return false; // Изменили возврат
|
||||
return false;
|
||||
}
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
// this.ownerIdentifier устанавливается в GameManager
|
||||
} else {
|
||||
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
@ -151,7 +147,7 @@ class GameInstance {
|
||||
}
|
||||
} else {
|
||||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||||
return false; // Изменили возврат
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,10 +178,10 @@ class GameInstance {
|
||||
|
||||
const charData = dataUtils.getCharacterData(actualCharacterKey);
|
||||
console.log(`[GameInstance ${this.id}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${this.ownerIdentifier}`);
|
||||
return true; // Успешное добавление
|
||||
return true;
|
||||
}
|
||||
|
||||
removePlayer(socketId, reason = "unknown_reason_for_removal") {
|
||||
removePlayer(socketId, reason = "unknown_reason_for_removal") { /* ... Код без изменений ... */
|
||||
const playerInfo = this.players[socketId];
|
||||
if (playerInfo) {
|
||||
const playerRole = playerInfo.id;
|
||||
@ -222,8 +218,7 @@ class GameInstance {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
|
||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { /* ... Код без изменений, вызывает turnTimer.pause() ... */
|
||||
console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`);
|
||||
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||
|
||||
@ -241,8 +236,8 @@ class GameInstance {
|
||||
|
||||
const otherPlayerRole = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
const otherSocket = this.playerSockets[otherPlayerRole];
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole); // Получаем запись другого игрока
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { // Уведомляем только если другой активен
|
||||
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole);
|
||||
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
|
||||
otherSocket.emit('opponentDisconnected', {
|
||||
disconnectedPlayerId: playerIdRole,
|
||||
disconnectedCharacterName: disconnectedName,
|
||||
@ -280,7 +275,7 @@ class GameInstance {
|
||||
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
|
||||
}
|
||||
|
||||
handlePlayerReconnected(playerIdRole, newSocket) {
|
||||
handlePlayerReconnected(playerIdRole, newSocket) { /* ... Код без изменений, вызывает turnTimer.resume() или start() ... */
|
||||
const identifier = newSocket.userData?.userId;
|
||||
console.log(`[GameInstance ${this.id}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`);
|
||||
|
||||
@ -356,11 +351,11 @@ class GameInstance {
|
||||
newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."}); return false;
|
||||
}
|
||||
if (!this.gameState) { if (!this.initializeGame()) {this._handleCriticalError('reconnect_same_socket_no_gs','GS null on same socket'); return false;} }
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); /* ... как выше ... */
|
||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey);
|
||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||
newSocket.emit('gameStarted', { /* ... как выше ... */
|
||||
newSocket.emit('gameStarted', {
|
||||
gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState,
|
||||
playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats,
|
||||
playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities,
|
||||
@ -380,18 +375,16 @@ class GameInstance {
|
||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||
}
|
||||
delete this.reconnectTimers[playerIdRole];
|
||||
// console.log(`[GameInstance ${this.id}] Reconnect timer & interval for role ${playerIdRole} cleared.`);
|
||||
}
|
||||
}
|
||||
clearAllReconnectTimers() { /* ... Код без изменений ... */
|
||||
// console.log(`[GameInstance ${this.id}] Clearing ALL reconnect timers.`);
|
||||
for (const roleId in this.reconnectTimers) {
|
||||
this.clearReconnectTimer(roleId);
|
||||
}
|
||||
}
|
||||
isGameEffectivelyPaused() { /* ... Код без изменений ... */
|
||||
if (this.mode === 'pvp') {
|
||||
if (this.playerCount < 2 && Object.keys(this.players).length > 0) { // Если игроков меньше двух, но хотя бы один есть
|
||||
if (this.playerCount < 2 && Object.keys(this.players).length > 0) {
|
||||
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||||
if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) {
|
||||
@ -412,7 +405,7 @@ class GameInstance {
|
||||
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_v2', 'AI game init: Human player not found or not active.'); return false; }
|
||||
if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_v3', 'AI game init: Human player not found or not active.'); return false; }
|
||||
this.playerCharacterKey = p1Entry.chosenCharacterKey;
|
||||
this.opponentCharacterKey = 'balard';
|
||||
} else {
|
||||
@ -420,8 +413,8 @@ class GameInstance {
|
||||
this.opponentCharacterKey = p2Entry ? p2Entry.chosenCharacterKey : null;
|
||||
|
||||
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) {
|
||||
console.error(`[GameInstance ${this.id}] PvP init error: playerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}. P1Info: ${!!p1Entry}, P2Info: ${!!p2Entry}`);
|
||||
this._handleCriticalError('init_pvp_char_key_missing_v2', `PvP init: playerCount is 2, but a charKey is missing.`);
|
||||
console.error(`[GameInstance ${this.id}] PvP init error: playerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
|
||||
this._handleCriticalError('init_pvp_char_key_missing_v3', `PvP init: playerCount is 2, but a charKey is missing.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -430,10 +423,10 @@ class GameInstance {
|
||||
const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
|
||||
|
||||
const isPlayerSlotFilledAndActive = !!playerData;
|
||||
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry));
|
||||
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry)); // p2Entry будет null если его нет
|
||||
|
||||
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) {
|
||||
this._handleCriticalError('init_ai_data_fail_gs_v2', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false;
|
||||
this._handleCriticalError('init_ai_data_fail_gs_v3', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false;
|
||||
}
|
||||
|
||||
this.logBuffer = [];
|
||||
@ -451,11 +444,8 @@ class GameInstance {
|
||||
gameMode: this.mode
|
||||
};
|
||||
|
||||
if (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) {
|
||||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
}
|
||||
|
||||
console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name} (Key: ${this.playerCharacterKey}). Opponent: ${this.gameState.opponent.name} (Key: ${this.opponentCharacterKey}). Ready for start: ${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
|
||||
// Не добавляем "Новая битва начинается" здесь, это будет в startGame, когда точно оба готовы
|
||||
console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name}. Opponent: ${this.gameState.opponent.name}. Ready for start if both active: ${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
|
||||
return isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive;
|
||||
}
|
||||
|
||||
@ -484,30 +474,35 @@ class GameInstance {
|
||||
console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`);
|
||||
return;
|
||||
}
|
||||
// Перед стартом игры, убедимся, что gameState полностью инициализирован и содержит обоих персонажей.
|
||||
// initializeGame должен был это сделать, но на всякий случай.
|
||||
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
|
||||
console.warn(`[GameInstance ${this.id}] startGame: GS or char keys not fully initialized. PKey: ${this.gameState?.player?.characterKey}, OKey: ${this.gameState?.opponent?.characterKey}. Attempting re-init.`);
|
||||
console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init one last time.`);
|
||||
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
|
||||
this._handleCriticalError('start_game_reinit_failed_sg_v3', 'Re-init before start failed or keys still missing.');
|
||||
this._handleCriticalError('start_game_reinit_failed_sg_v4', 'Re-initialization before start failed or keys still missing in gameState.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`[GameInstance ${this.id}] Starting game. Player in GS: ${this.gameState.player.name}, Opponent in GS: ${this.gameState.opponent.name}`);
|
||||
console.log(`[GameInstance ${this.id}] Starting game. Player in GS: ${this.gameState.player.name} (${this.playerCharacterKey}), Opponent in GS: ${this.gameState.opponent.name} (${this.opponentCharacterKey})`);
|
||||
|
||||
const pData = dataUtils.getCharacterData(this.playerCharacterKey);
|
||||
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
|
||||
|
||||
if (!pData || !oData) {
|
||||
this._handleCriticalError('start_char_data_fail_sg_v4', `Failed to load char data at game start. P: ${!!pData}, O: ${!!oData}`);
|
||||
this._handleCriticalError('start_char_data_fail_sg_v5', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем лог о начале битвы здесь, когда уверены, что оба игрока есть
|
||||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
|
||||
// --- Начальные насмешки ---
|
||||
// Убеждаемся, что объекты gameState.player и gameState.opponent существуют и имеют characterKey
|
||||
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
||||
// Убедимся, что gameState.player и .opponent существуют для передачи в _sayTaunt
|
||||
if (this.gameState.player && this.gameState.opponent) {
|
||||
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
|
||||
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
|
||||
}
|
||||
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
|
||||
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
|
||||
} else {
|
||||
console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready. GSPlayer: ${this.gameState.player?.name}, GSOpponent: ${this.gameState.opponent?.name}`);
|
||||
}
|
||||
|
||||
const initialLog = this.consumeLogBuffer();
|
||||
@ -542,7 +537,6 @@ class GameInstance {
|
||||
if (!actingPlayerInfo || !actingPlayerInfo.socket) {
|
||||
console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return;
|
||||
}
|
||||
// const requestingSocketId = actingPlayerInfo.socket.id;
|
||||
|
||||
if (this.isGameEffectivelyPaused()) {
|
||||
actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."});
|
||||
@ -574,16 +568,11 @@ class GameInstance {
|
||||
this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack');
|
||||
gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
|
||||
actionIsValidAndPerformed = true;
|
||||
const delayedBuff = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK) && !eff.justCast);
|
||||
if (delayedBuff) {
|
||||
const manaRegenConfig = GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN || 0;
|
||||
const regen = Math.min(manaRegenConfig, attackerData.baseStats.maxResource - attackerState.currentResource);
|
||||
if (regen > 0) {
|
||||
attackerState.currentResource = Math.round(attackerState.currentResource + regen);
|
||||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${regen} ${attackerState.resourceName} от "${delayedBuff.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
|
||||
}
|
||||
delayedBuff.turnsLeft = 0;
|
||||
}
|
||||
// --- ИСПРАВЛЕНИЕ ДЛЯ СИЛЫ ПРИРОДЫ ---
|
||||
// Логика бонуса (реген маны) теперь полностью внутри performAttack в combatLogic.js.
|
||||
// GameInstance НЕ ДОЛЖЕН здесь "потреблять" эффект (обнулять turnsLeft или удалять).
|
||||
// Длительность эффекта управляется в effectsLogic.js.
|
||||
// --- КОНЕЦ ИСПРАВЛЕНИЯ ---
|
||||
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
|
||||
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
|
||||
if (!ability) {
|
||||
@ -823,7 +812,7 @@ class GameInstance {
|
||||
this.gameManager._cleanupGame(this.id, "player_surrendered");
|
||||
}
|
||||
|
||||
handleTurnTimeout() { /* ... Код без изменений, с вызовом _sayTaunt ... */
|
||||
handleTurnTimeout() { /* ... Код без изменений ... */
|
||||
if (!this.gameState || this.gameState.isGameOver) return;
|
||||
console.log(`[GameInstance ${this.id}] Turn timeout occurred.`);
|
||||
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
@ -840,7 +829,7 @@ class GameInstance {
|
||||
this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown' });
|
||||
this.gameManager._cleanupGame(this.id, `timeout_${result.reason}`);
|
||||
}
|
||||
_handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений, с переводом логов ... */
|
||||
_handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений ... */
|
||||
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`);
|
||||
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
|
||||
else if (!this.gameState) this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
|
||||
|
@ -1,15 +1,13 @@
|
||||
// /server/game/logic/combatLogic.js
|
||||
|
||||
// Предполагается, что gameLogic.getRandomTaunt и dataUtils будут доступны
|
||||
// через параметры, передаваемые из GameInstance, или через объект gameLogic.
|
||||
// const GAME_CONFIG_STATIC = require('../../core/config'); // Можно, если нужно
|
||||
// GAME_CONFIG и dataUtils будут передаваться в функции как параметры.
|
||||
|
||||
/**
|
||||
* Обрабатывает базовую атаку одного бойца по другому.
|
||||
* @param {object} attackerState - Состояние атакующего бойца из gameState.
|
||||
* @param {object} defenderState - Состояние защищающегося бойца из gameState.
|
||||
* @param {object} attackerBaseStats - Базовые статы атакующего.
|
||||
* @param {object} defenderBaseStats - Базовые статы защищающегося.
|
||||
* @param {object} attackerBaseStats - Базовые статы атакующего (из dataUtils.getCharacterBaseStats).
|
||||
* @param {object} defenderBaseStats - Базовые статы защищающегося (из dataUtils.getCharacterBaseStats).
|
||||
* @param {object} currentGameState - Текущее полное состояние игры.
|
||||
* @param {function} addToLogCallback - Функция для добавления сообщений в лог игры.
|
||||
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
||||
@ -24,35 +22,97 @@ function performAttack(
|
||||
currentGameState,
|
||||
addToLogCallback,
|
||||
configToUse,
|
||||
dataUtils, // Добавлен dataUtils
|
||||
getRandomTauntFunction // Добавлена функция для насмешек
|
||||
dataUtils,
|
||||
getRandomTauntFunction
|
||||
) {
|
||||
// Расчет базового урона с вариацией
|
||||
let damage = Math.floor(
|
||||
attackerBaseStats.attackPower *
|
||||
(configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)
|
||||
);
|
||||
let wasBlocked = false;
|
||||
let attackBonusesLog = []; // Для сбора информации о бонусах к атаке
|
||||
|
||||
// --- ПРОВЕРКА И ПРИМЕНЕНИЕ БОНУСА ОТ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
||||
// Ищем активный бафф, который должен сработать ПРИ атаке
|
||||
const delayedAttackBuff = attackerState.activeEffects.find(eff =>
|
||||
eff.isDelayed &&
|
||||
(eff.id === configToUse.ABILITY_ID_NATURE_STRENGTH || eff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) &&
|
||||
eff.turnsLeft > 0 &&
|
||||
!eff.justCast
|
||||
);
|
||||
|
||||
if (delayedAttackBuff) {
|
||||
console.log(`[CombatLogic performAttack] Found active delayed buff: ${delayedAttackBuff.name} for ${attackerState.name}`);
|
||||
|
||||
// 1. Применяем бонус к урону (если он есть в конфиге/данных эффекта)
|
||||
let damageBonus = 0;
|
||||
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
|
||||
// Предположим, что Сила Природы НЕ дает прямого бонуса к урону атаки, а только реген маны.
|
||||
// Если бы давала, то: damageBonus = configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS || 0;
|
||||
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
|
||||
// Аналогично для Альмагест
|
||||
// damageBonus = configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS || 0;
|
||||
}
|
||||
if (damageBonus > 0) {
|
||||
damage += damageBonus;
|
||||
attackBonusesLog.push(`урон +${damageBonus} от "${delayedAttackBuff.name}"`);
|
||||
}
|
||||
|
||||
// 2. Восстановление ресурса (для Силы Природы / Усиления Тьмой)
|
||||
// Этот бонус (восстановление ресурса) срабатывает при каждой атаке, пока эффект активен
|
||||
let resourceRegenConfigKey = null;
|
||||
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
|
||||
resourceRegenConfigKey = 'NATURE_STRENGTH_MANA_REGEN';
|
||||
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
|
||||
// Предположим, аналогичный конфиг для Альмагест, если она тоже регенит ресурс при атаке под баффом
|
||||
resourceRegenConfigKey = 'ALMAGEST_DARK_ENERGY_REGEN';
|
||||
}
|
||||
|
||||
if (resourceRegenConfigKey && configToUse[resourceRegenConfigKey]) {
|
||||
const regenAmount = configToUse[resourceRegenConfigKey];
|
||||
const actualRegen = Math.min(regenAmount, attackerBaseStats.maxResource - attackerState.currentResource);
|
||||
if (actualRegen > 0) {
|
||||
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
|
||||
if (addToLogCallback) {
|
||||
addToLogCallback(
|
||||
`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от "${delayedAttackBuff.name}"!`,
|
||||
configToUse.LOG_TYPE_HEAL
|
||||
);
|
||||
}
|
||||
// Не добавляем в attackBonusesLog, т.к. это отдельное событие, уже залогированное
|
||||
}
|
||||
}
|
||||
|
||||
// Важно: НЕ МЕНЯЕМ здесь delayedAttackBuff.turnsLeft и НЕ УДАЛЯЕМ эффект.
|
||||
// Его длительность будет уменьшаться в effectsLogic.processEffects каждый ход владельца эффекта.
|
||||
}
|
||||
// --- КОНЕЦ ПРОВЕРКИ И ПРИМЕНЕНИЯ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
||||
|
||||
// Проверка на блок
|
||||
if (defenderState.isBlocking) {
|
||||
const initialDamage = damage;
|
||||
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||||
wasBlocked = true;
|
||||
if (addToLogCallback) {
|
||||
addToLogCallback(
|
||||
`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`,
|
||||
configToUse.LOG_TYPE_BLOCK
|
||||
);
|
||||
let blockLogMsg = `🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`;
|
||||
if (attackBonusesLog.length > 0) {
|
||||
blockLogMsg += ` (${attackBonusesLog.join(', ')})`;
|
||||
}
|
||||
addToLogCallback(blockLogMsg, configToUse.LOG_TYPE_BLOCK);
|
||||
}
|
||||
} else {
|
||||
if (addToLogCallback) {
|
||||
addToLogCallback(
|
||||
`${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`,
|
||||
configToUse.LOG_TYPE_DAMAGE
|
||||
);
|
||||
let hitLogMsg = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
|
||||
if (attackBonusesLog.length > 0) {
|
||||
hitLogMsg += ` (${attackBonusesLog.join(', ')})`;
|
||||
}
|
||||
addToLogCallback(hitLogMsg, configToUse.LOG_TYPE_DAMAGE);
|
||||
}
|
||||
}
|
||||
|
||||
const actualDamageDealt = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage));
|
||||
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||||
const actualDamageDealtToHp = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage)); // Сколько HP реально отнято
|
||||
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
||||
|
||||
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
|
||||
@ -60,20 +120,20 @@ function performAttack(
|
||||
let reactionTauntTrigger = null;
|
||||
if (wasBlocked) {
|
||||
reactionTauntTrigger = 'onOpponentAttackBlocked';
|
||||
} else if (actualDamageDealt > 0) { // Если урон прошел
|
||||
} else if (actualDamageDealtToHp > 0) {
|
||||
reactionTauntTrigger = 'onOpponentAttackHit';
|
||||
}
|
||||
// Можно добавить 'onOpponentAttackMissed' если actualDamageDealt === 0 и !wasBlocked
|
||||
// Можно добавить еще условие для промаха, если урон = 0 и не было блока
|
||||
|
||||
if (reactionTauntTrigger) {
|
||||
const attackerFullData = dataUtils.getCharacterData(attackerState.characterKey);
|
||||
if (attackerFullData) { // Убедимся, что данные атакующего есть
|
||||
const attackerFullDataForTaunt = dataUtils.getCharacterData(attackerState.characterKey);
|
||||
if (attackerFullDataForTaunt) {
|
||||
const reactionTaunt = getRandomTauntFunction(
|
||||
defenderState.characterKey, // Кто говорит (защищающийся)
|
||||
reactionTauntTrigger, // Триггер (onOpponentAttackBlocked или onOpponentAttackHit)
|
||||
{}, // Контекст (пока пустой для этих реакций)
|
||||
defenderState.characterKey,
|
||||
reactionTauntTrigger,
|
||||
{},
|
||||
configToUse,
|
||||
attackerFullData, // Оппонент для говорящего - это атакующий
|
||||
attackerFullDataForTaunt, // Оппонент для говорящего (защитника) - это атакующий
|
||||
currentGameState
|
||||
);
|
||||
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||||
@ -97,7 +157,7 @@ function performAttack(
|
||||
* @param {object} configToUse - Конфигурация игры.
|
||||
* @param {object} dataUtils - Утилиты для доступа к данным игры.
|
||||
* @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt.
|
||||
* @param {function} checkIfActionWasSuccessfulFunction - Функция для проверки успеха действия (для контекстных насмешек).
|
||||
* @param {function|null} checkIfActionWasSuccessfulFunction - (Опционально) Функция для проверки успеха действия для контекстных насмешек.
|
||||
*/
|
||||
function applyAbilityEffect(
|
||||
ability,
|
||||
@ -108,14 +168,13 @@ function applyAbilityEffect(
|
||||
currentGameState,
|
||||
addToLogCallback,
|
||||
configToUse,
|
||||
dataUtils, // Добавлен dataUtils
|
||||
getRandomTauntFunction, // Добавлена функция для насмешек
|
||||
checkIfActionWasSuccessfulFunction // Добавлена функция для проверки успеха
|
||||
dataUtils,
|
||||
getRandomTauntFunction,
|
||||
checkIfActionWasSuccessfulFunction // Пока не используется активно, outcome определяется внутри
|
||||
) {
|
||||
let abilityApplicationSucceeded = true; // Флаг для отслеживания, применилась ли способность успешно (для контекста насмешек)
|
||||
let actionOutcomeForTaunt = null; // 'success' или 'fail' для способностей типа безмолвия
|
||||
let abilityApplicationSucceeded = true;
|
||||
let actionOutcomeForTaunt = null; // 'success' или 'fail'
|
||||
|
||||
// --- Основная логика применения способности ---
|
||||
switch (ability.type) {
|
||||
case configToUse.ACTION_TYPE_HEAL:
|
||||
const healAmount = Math.floor(ability.power * (configToUse.HEAL_VARIATION_MIN + Math.random() * configToUse.HEAL_VARIATION_RANGE));
|
||||
@ -125,7 +184,7 @@ function applyAbilityEffect(
|
||||
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
|
||||
} else {
|
||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.`, configToUse.LOG_TYPE_INFO);
|
||||
abilityApplicationSucceeded = false; // Можно считать это "неудачей" для реакции, если хотите
|
||||
abilityApplicationSucceeded = false;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -142,21 +201,21 @@ function applyAbilityEffect(
|
||||
if (addToLogCallback && !wasAbilityBlocked) {
|
||||
addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE);
|
||||
}
|
||||
if (damage <= 0 && !wasAbilityBlocked) abilityApplicationSucceeded = false; // Если урона не было (например, из-за эффектов)
|
||||
if (damage <= 0 && !wasAbilityBlocked) abilityApplicationSucceeded = false;
|
||||
break;
|
||||
|
||||
case configToUse.ACTION_TYPE_BUFF:
|
||||
let effectDescriptionBuff = ability.description;
|
||||
if (typeof ability.descriptionFunction === 'function') {
|
||||
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats); // targetBaseStats здесь оппонент кастера
|
||||
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats);
|
||||
}
|
||||
casterState.activeEffects.push({
|
||||
id: ability.id, name: ability.name, description: effectDescriptionBuff,
|
||||
type: ability.type, duration: ability.duration,
|
||||
turnsLeft: ability.duration,
|
||||
turnsLeft: ability.duration, // Эффект начнет тикать в конце текущего хода кастера
|
||||
grantsBlock: !!ability.grantsBlock,
|
||||
isDelayed: !!ability.isDelayed,
|
||||
justCast: true
|
||||
isDelayed: !!ability.isDelayed, // Важно для "Силы Природы"
|
||||
justCast: true // Помечаем, что только что наложен
|
||||
});
|
||||
if (ability.grantsBlock) require('./effectsLogic').updateBlockingStatus(casterState);
|
||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
|
||||
@ -164,8 +223,6 @@ function applyAbilityEffect(
|
||||
|
||||
case configToUse.ACTION_TYPE_DISABLE:
|
||||
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
|
||||
// ... (логика полного безмолвия как у вас)
|
||||
// Установите actionOutcomeForTaunt = 'success' или 'fail' если нужно
|
||||
const effectIdFullSilence = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
|
||||
if (!targetState.activeEffects.some(e => e.id === effectIdFullSilence)) {
|
||||
targetState.activeEffects.push({
|
||||
@ -178,14 +235,13 @@ function applyAbilityEffect(
|
||||
} else {
|
||||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO);
|
||||
abilityApplicationSucceeded = false;
|
||||
actionOutcomeForTaunt = 'fail'; // Считаем провалом, если уже активен
|
||||
actionOutcomeForTaunt = 'fail';
|
||||
}
|
||||
}
|
||||
else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') {
|
||||
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
|
||||
actionOutcomeForTaunt = success ? 'success' : 'fail'; // Устанавливаем для контекста насмешки
|
||||
actionOutcomeForTaunt = success ? 'success' : 'fail';
|
||||
if (success) {
|
||||
// ... (ваша логика наложения безмолвия на способность)
|
||||
const targetAbilitiesList = dataUtils.getCharacterAbilities(targetState.characterKey);
|
||||
const availableAbilitiesToSilence = targetAbilitiesList.filter(pa =>
|
||||
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
|
||||
@ -194,7 +250,7 @@ function applyAbilityEffect(
|
||||
if (availableAbilitiesToSilence.length > 0) {
|
||||
const abilityToSilence = availableAbilitiesToSilence[Math.floor(Math.random() * availableAbilitiesToSilence.length)];
|
||||
const turns = configToUse.SILENCE_DURATION;
|
||||
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 });
|
||||
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); // +1 т.к. уменьшится в конце хода цели
|
||||
targetState.activeEffects.push({
|
||||
id: `playerSilencedOn_${abilityToSilence.id}`, name: `Безмолвие: ${abilityToSilence.name}`,
|
||||
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
|
||||
@ -204,7 +260,7 @@ function applyAbilityEffect(
|
||||
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" у ${targetBaseStats.name} заблокировано на ${turns} хода!`, configToUse.LOG_TYPE_EFFECT);
|
||||
} else {
|
||||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!`, configToUse.LOG_TYPE_INFO);
|
||||
actionOutcomeForTaunt = 'fail'; // Провал, если нечего глушить
|
||||
actionOutcomeForTaunt = 'fail';
|
||||
}
|
||||
} else {
|
||||
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!`, configToUse.LOG_TYPE_INFO);
|
||||
@ -213,8 +269,6 @@ function applyAbilityEffect(
|
||||
break;
|
||||
|
||||
case configToUse.ACTION_TYPE_DEBUFF:
|
||||
// ... (логика дебаффа как у вас)
|
||||
// Установите actionOutcomeForTaunt если нужно
|
||||
const effectIdDebuff = 'effect_' + ability.id;
|
||||
if (!targetState.activeEffects.some(e => e.id === effectIdDebuff)) {
|
||||
let effectDescriptionDebuff = ability.description;
|
||||
@ -237,7 +291,6 @@ function applyAbilityEffect(
|
||||
break;
|
||||
|
||||
case configToUse.ACTION_TYPE_DRAIN:
|
||||
// ... (логика дрейна как у вас)
|
||||
if (casterState.characterKey === 'balard') {
|
||||
let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0;
|
||||
if (ability.powerDamage > 0) {
|
||||
@ -262,7 +315,7 @@ function applyAbilityEffect(
|
||||
else if (damageDealtDrain > 0) logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
||||
else logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
||||
if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
|
||||
if (manaDrained <= 0 && damageDealtDrain <=0) abilityApplicationSucceeded = false;
|
||||
if (manaDrained <= 0 && damageDealtDrain <=0 && healthGained <=0) abilityApplicationSucceeded = false;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -273,32 +326,37 @@ function applyAbilityEffect(
|
||||
}
|
||||
|
||||
// --- Насмешка от цели (targetState) в ответ на применение способности ---
|
||||
// Вызываем только если способность не была нацелена на самого себя и успешно применилась (или как вы решите)
|
||||
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id && abilityApplicationSucceeded) {
|
||||
const casterFullData = dataUtils.getCharacterData(casterState.characterKey);
|
||||
if (casterFullData) { // Убедимся, что данные кастера есть
|
||||
// Вызываем только если способность не была нацелена на самого себя
|
||||
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id) {
|
||||
const casterFullDataForTaunt = dataUtils.getCharacterData(casterState.characterKey);
|
||||
if (casterFullDataForTaunt) {
|
||||
let tauntContext = { abilityId: ability.id };
|
||||
if (actionOutcomeForTaunt) { // Если для этой способности важен исход (success/fail)
|
||||
// Если для этой способности был определен исход (например, для безмолвия Баларда), используем его
|
||||
if (actionOutcomeForTaunt) {
|
||||
tauntContext.outcome = actionOutcomeForTaunt;
|
||||
} else if (checkIfActionWasSuccessfulFunction) {
|
||||
// Если есть общая функция проверки успеха (менее специфично, чем actionOutcomeForTaunt)
|
||||
// Это пример, вам нужно реализовать checkIfActionWasSuccessfulFunction
|
||||
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
|
||||
// tauntContext.outcome = success ? 'success' : 'fail';
|
||||
}
|
||||
// Здесь можно было бы вызвать checkIfActionWasSuccessfulFunction, если бы он был и нужен для других способностей
|
||||
// else if (checkIfActionWasSuccessfulFunction) {
|
||||
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
|
||||
// tauntContext.outcome = success ? 'success' : 'fail';
|
||||
// }
|
||||
|
||||
|
||||
// Вызываем насмешку, только если основное применение способности не считается полным провалом (опционально)
|
||||
// Либо всегда вызываем, и пусть tauntLogic решает, есть ли реакция на "провальную" абилку
|
||||
// if (abilityApplicationSucceeded || actionOutcomeForTaunt === 'fail') { // Например, реагируем даже на провал Эха Безмолвия
|
||||
const reactionTaunt = getRandomTauntFunction(
|
||||
targetState.characterKey, // Кто говорит (цель способности)
|
||||
'onOpponentAction', // Триггер
|
||||
tauntContext, // Контекст: ID способности и исход (если нужен)
|
||||
tauntContext, // Контекст: ID способности и, возможно, outcome
|
||||
configToUse,
|
||||
casterFullData, // Оппонент для говорящего - это кастер
|
||||
casterFullDataForTaunt, // Оппонент для говорящего - это кастер
|
||||
currentGameState
|
||||
);
|
||||
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||||
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -306,16 +364,10 @@ function applyAbilityEffect(
|
||||
|
||||
/**
|
||||
* Проверяет валидность использования способности.
|
||||
* @param {object} ability - Объект способности.
|
||||
* @param {object} casterState - Состояние кастера.
|
||||
* @param {object} targetState - Состояние цели.
|
||||
* @param {object} configToUse - Конфигурация игры.
|
||||
* @returns {{isValid: boolean, reason: string|null}} Результат проверки.
|
||||
*/
|
||||
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
||||
// ... (существующий код checkAbilityValidity без изменений) ...
|
||||
// ... (код checkAbilityValidity без изменений, как вы предоставили) ...
|
||||
if (!ability) return { isValid: false, reason: "Способность не найдена." };
|
||||
|
||||
if (casterState.currentResource < ability.cost) {
|
||||
return { isValid: false, reason: `${casterState.name} пытается применить "${ability.name}", но не хватает ${casterState.resourceName}!` };
|
||||
}
|
||||
@ -330,22 +382,21 @@ function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
||||
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
|
||||
}
|
||||
}
|
||||
|
||||
const isCasterFullySilenced = casterState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||||
const isAbilitySpecificallySilenced = casterState.disabledAbilities?.some(dis => dis.abilityId === ability.id && dis.turnsLeft > 0);
|
||||
if (isCasterFullySilenced || isAbilitySpecificallySilenced) {
|
||||
return { isValid: false, reason: `${casterState.name} не может использовать способности из-за безмолвия!` };
|
||||
}
|
||||
|
||||
if (ability.type === configToUse.ACTION_TYPE_BUFF && casterState.activeEffects.some(e => e.id === ability.id)) {
|
||||
return { isValid: false, reason: `Эффект "${ability.name}" уже активен!` };
|
||||
// Исключение для Силы Природы и Усиления Тьмой - их можно обновлять, если isDelayed
|
||||
if (!ability.isDelayed) {
|
||||
return { isValid: false, reason: `Эффект "${ability.name}" уже активен!` };
|
||||
}
|
||||
}
|
||||
|
||||
const isTargetedDebuff = ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF;
|
||||
if (isTargetedDebuff && targetState.activeEffects.some(e => e.id === 'effect_' + ability.id)) {
|
||||
return { isValid: false, reason: `Эффект "${ability.name}" уже наложен на ${targetState.name}!` };
|
||||
}
|
||||
|
||||
return { isValid: true, reason: null };
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user