bc/public/js/ui.js

665 lines
53 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.

// /public/js/ui.js
// Этот файл отвечает за обновление DOM на основе состояния игры,
// полученного от client.js (который, в свою очередь, получает его от сервера).
(function() {
// --- DOM Элементы ---
const uiElements = {
player: { // Панель для персонажа, которым управляет ЭТОТ клиент
panel: document.getElementById('player-panel'),
name: document.getElementById('player-name'),
avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'),
resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'),
status: document.getElementById('player-status'),
effectsContainer: document.getElementById('player-effects'),
buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
},
opponent: { // Панель для персонажа-противника ЭТОГО клиента
panel: document.getElementById('opponent-panel'),
name: document.getElementById('opponent-name'),
avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'),
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
status: document.getElementById('opponent-status'),
effectsContainer: document.getElementById('opponent-effects'),
buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
},
controls: {
turnIndicator: document.getElementById('turn-indicator'),
buttonAttack: document.getElementById('button-attack'),
buttonBlock: document.getElementById('button-block'), // Защита пока не активна
abilitiesGrid: document.getElementById('abilities-grid'),
},
log: {
list: document.getElementById('log-list'),
},
gameOver: {
screen: document.getElementById('game-over-screen'),
message: document.getElementById('result-message'),
// restartButton: document.getElementById('restart-game-button'), // Старый ID, заменен
returnToMenuButton: document.getElementById('return-to-menu-button'), // Новый ID
modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
},
gameHeaderTitle: document.querySelector('.game-header h1'),
playerResourceTypeIcon: document.getElementById('player-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'),
opponentResourceTypeIcon: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'),
playerResourceBarContainer: document.getElementById('player-resource-bar')?.closest('.stat-bar-container'),
opponentResourceBarContainer: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container'),
};
function addToLog(message, type = 'info') {
const logListElement = uiElements.log.list;
if (!logListElement) return;
const li = document.createElement('li');
li.textContent = message;
const config = window.GAME_CONFIG || {}; // Получаем конфиг из глобальной области
// Формируем класс для лога на основе типа (используем константы из конфига или фоллбэк)
const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
li.className = logTypeClass;
logListElement.appendChild(li);
// Прокрутка лога вниз
requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; });
}
function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) {
const elements = uiElements[panelRole]; // 'player' или 'opponent'
const config = window.GAME_CONFIG || {};
// Базовая проверка наличия необходимых элементов и данных
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
// console.warn(`updateFighterPanelUI: Отсутствуют элементы UI, состояние бойца или базовые статы для панели ${panelRole}.`);
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
if (elements && elements.panel && elements.panel.style.display !== 'none') {
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
// elements.panel.style.opacity = '0.5'; // Пример: сделать полупрозрачной, если нет данных
}
return;
}
// Если панель была полупрозрачной (из-за отсутствия данных), а теперь данные есть, делаем ее полностью видимой
// if (elements.panel && elements.panel.style.opacity !== '1' && fighterState && fighterBaseStats) {
// elements.panel.style.opacity = '1';
// }
// Обновление имени и иконки персонажа
if (elements.name) {
let iconClass = 'fa-question'; // Иконка по умолчанию
// let accentColor = 'var(--text-muted)'; // Цвет по умолчанию - теперь берется из CSS через классы иконок
const characterKey = fighterBaseStats.characterKey;
// Определяем класс иконки в зависимости от персонажа
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; } // icon-player имеет цвет через CSS
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; } // icon-almagest имеет цвет через CSS
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; } // icon-opponent имеет цвет через CSS
else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки имени.`); */ }
// Обновляем innerHTML имени, включая иконку и текст. Добавляем "(Вы)" для управляемого персонажа.
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
if (isControlledByThisClient) nameHtml += " (Вы)";
elements.name.innerHTML = nameHtml;
// Цвет имени теперь задается CSS через классы icon-player/opponent/almagest, примененные к самой иконке
// elements.name.style.color = accentColor; // Эту строку можно удалить, если цвет задан через CSS
}
// Обновление аватара
if (elements.avatar && fighterBaseStats.avatarPath) {
elements.avatar.src = fighterBaseStats.avatarPath;
// Обновляем рамку аватара в зависимости от персонажа
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard'); // Убираем старые классы
elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`); // Добавляем класс для текущего персонажа
} else if (elements.avatar) {
elements.avatar.src = 'images/default_avatar.png'; // Запасной аватар
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard'); // Убираем старые классы
}
// Обновление полос здоровья и ресурса
const maxHp = Math.max(1, fighterBaseStats.maxHp); // Избегаем деления на ноль
const maxRes = Math.max(1, fighterBaseStats.maxResource);
const currentHp = Math.max(0, fighterState.currentHp);
const currentRes = Math.max(0, fighterState.currentResource);
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
// ИСПРАВЛЕНО: Убрано округление для отображения текущего ресурса
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`; // <-- ИСПРАВЛЕНО
// Обновление типа ресурса и иконки (mana/stamina/dark-energy)
const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer;
const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy'); // Сначала удаляем все классы ресурсов
let resourceClass = 'mana'; let iconClass = 'fa-flask'; // Значения по умолчанию (для Маны)
if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; } // Или другую иконку для темной энергии
resourceBarContainerToUpdate.classList.add(resourceClass);
resourceIconElementToUpdate.className = `fas ${iconClass}`; // Обновляем класс иконки
}
// Обновление статуса (Готов/Защищается)
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
elements.status.textContent = statusText;
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking); // Применяем класс для стилизации статуса "Защищается"
// Обновление подсветки и рамки панели (в зависимости от персонажа)
if (elements.panel) {
let borderColorVar = 'var(--panel-border)'; // Цвет по умолчанию
// Снимаем все старые классы для рамки
elements.panel.classList.remove('panel-elena', 'panel-almagest', 'panel-balard');
// Применяем класс для рамки в зависимости от персонажа
if (fighterBaseStats.characterKey === 'elena') { elements.panel.classList.add('panel-elena'); borderColorVar = 'var(--accent-player)'; } // Цвет рамки через CSS переменную
else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки через CSS переменную
else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; } // Цвет рамки через CSS переменную
// Обновляем тень (свечение). Цвет свечения тоже может быть переменной.
let glowColorVar = 'rgba(0, 0, 0, 0.4)'; // Базовая тень
if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
else if (fighterBaseStats.characterKey === 'almagest' || fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)'; // Используем одну тень для всех оппонентов (Балард/Альмагест)
// Устанавливаем рамку и тень
elements.panel.style.borderColor = borderColorVar;
// Используем переменную для свечения. Базовая тень inset оставлена как есть.
elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
}
}
function generateEffectsHTML(effectsArray) {
const config = window.GAME_CONFIG || {};
if (!effectsArray || effectsArray.length === 0) return 'Нет';
// Сортируем эффекты: сначала положительные (buff, block, heal), затем отрицательные (debuff, disable)
// иконка для стана/безмолвия, иконка для ослабления, иконка для усиления
const sortedEffects = [...effectsArray].sort((a, b) => {
const typeOrder = {
[config.ACTION_TYPE_BUFF]: 1,
grantsBlock: 2,
[config.ACTION_TYPE_HEAL]: 3, // HoT эффекты
[config.ACTION_TYPE_DEBUFF]: 4, // DoT, ресурсные дебаффы
[config.ACTION_TYPE_DISABLE]: 5 // Silence, Stun
};
// Определяем порядок для эффекта A
let orderA = typeOrder[a.type];
if (a.grantsBlock) orderA = typeOrder.grantsBlock;
if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
// Определяем порядок для эффекта B
let orderB = typeOrder[b.type];
if (b.grantsBlock) orderB = typeOrder.grantsBlock;
if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
return (orderA || 99) - (orderB || 99); // Сортируем по порядку, неизвестные типы в конец
});
return sortedEffects.map(eff => {
let effectClasses = config.CSS_CLASS_EFFECT || 'effect'; // Базовый класс для всех эффектов
// Формируем заголовок тултипа
const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
// Текст, отображаемый на самой плашке эффекта
const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
// Добавляем специфичные классы для стилизации по типу эффекта
if (eff.isFullSilence || eff.id.startsWith('playerSilencedOn_') || (eff.type === config.ACTION_TYPE_DISABLE)) { // Эффекты полного безмолвия или специфичного заглушения
effectClasses += ' effect-stun'; // Класс для стана/безмолвия
} else if (eff.grantsBlock) { // Эффекты, дающие блок
effectClasses += ' effect-block'; // Класс для эффектов блока
} else if (eff.type === config.ACTION_TYPE_DEBUFF) { // Явные дебаффы (например, сжигание ресурса, DoT)
effectClasses += ' effect-debuff'; // Класс для ослаблений
} else if (eff.type === config.ACTION_TYPE_BUFF || eff.type === config.ACTION_TYPE_HEAL) { // Явные баффы или эффекты HoT
effectClasses += ' effect-buff'; // Класс для усилений
} else {
// console.warn(`generateEffectsHTML: Неизвестный тип эффекта для стилизации: ${eff.type} (ID: ${eff.id})`);
effectClasses += ' effect-info'; // Класс по умолчанию или информационный
}
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
}).join(' '); // Объединяем все HTML-строки эффектов в одну
}
function updateEffectsUI(currentGameState) {
if (!currentGameState || !window.GAME_CONFIG) { return; }
const mySlotId = window.myPlayerId; // Технический ID слота этого клиента
const config = window.GAME_CONFIG;
if (!mySlotId) { return; }
const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : config.PLAYER_ID;
const myState = currentGameState[mySlotId]; // Состояние персонажа этого клиента
if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
// Разделяем эффекты на баффы и дебаффы для отображения
// Критерии разделения могут быть специфичны для игры:
// Баффы: тип BUFF, эффекты дающие блок (grantsBlock), эффекты лечения (HoT)
// Дебаффы: тип DEBUFF, тип DISABLE (кроме grantsBlock), эффекты урона (DoT)
// Включаем isFullSilence и playerSilencedOn_X в "дебаффы" для отображения (можно сделать отдельную категорию)
const myBuffs = myState.activeEffects.filter(e =>
e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === config.ACTION_TYPE_HEAL) // HoT как бафф
);
const myDebuffs = myState.activeEffects.filter(e =>
e.type === config.ACTION_TYPE_DEBUFF ||
e.type === config.ACTION_TYPE_DISABLE // Disable как дебафф
// || (e.type === config.ACTION_TYPE_DAMAGE) // DoT как дебафф, если есть
);
// Специально добавляем полные безмолвия и заглушения абилок в дебаффы, даже если их тип не DEBUFF/DISABLE
myDebuffs.push(...myState.activeEffects.filter(e => e.isFullSilence || e.id.startsWith('playerSilencedOn_') && !myDebuffs.some(d => d.id === e.id))); // Избегаем дублирования
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myBuffs);
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myDebuffs);
} else if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList) {
// Если нет активных эффектов или состояния, очищаем списки
uiElements.player.buffsList.innerHTML = 'Нет';
uiElements.player.debuffsList.innerHTML = 'Нет';
}
const opponentState = currentGameState[opponentSlotId]; // Состояние оппонента этого клиента
if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
const opponentBuffs = opponentState.activeEffects.filter(e =>
e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === config.ACTION_TYPE_HEAL) // HoT как бафф
);
const opponentDebuffs = opponentState.activeEffects.filter(e =>
e.type === config.ACTION_TYPE_DEBUFF ||
e.type === config.ACTION_TYPE_DISABLE // Disable как дебафф
// || (e.type === config.ACTION_TYPE_DAMAGE) // DoT как дебафф, если есть
);
// Специально добавляем полные безмолвия и заглушения абилок в дебаффы оппонента, даже если их тип не DEBUFF/DISABLE
opponentDebuffs.push(...opponentState.activeEffects.filter(e => e.isFullSilence || e.id.startsWith('playerSilencedOn_') && !opponentDebuffs.some(d => d.id === e.id))); // Избегаем дублирования
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentBuffs);
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentDebuffs);
} else if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList) {
// Если нет активных эффектов или состояния оппонента, очищаем списки
uiElements.opponent.buffsList.innerHTML = 'Нет';
uiElements.opponent.debuffsList.innerHTML = 'Нет';
}
}
function updateUI() {
const currentGameState = window.gameState; // Глобальное состояние игры
const gameDataGlobal = window.gameData; // Глобальные данные ( статы, абилки ) для этого клиента
const configGlobal = window.GAME_CONFIG; // Глобальный конфиг
const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
return;
}
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
console.warn("updateUI: Некоторые базовые uiElements не найдены.");
return;
}
// Определяем, чей сейчас ход по ID слота
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
// Определяем ID слота оппонента для этого клиента ( необходимо для определения, чьи панели обновлять как "мои" и "противника")
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
// Обновление панели "моего" персонажа ( которым управляет этот клиент)
const myStateInGameState = currentGameState[myActualPlayerId];
const myBaseStatsForUI = gameDataGlobal.playerBaseStats; // playerBaseStats в gameData - это всегда статы персонажа этого клиента
if (myStateInGameState && myBaseStatsForUI) {
if (uiElements.player.panel) uiElements.player.panel.style.opacity = '1'; // Делаем панель полностью видимой, если есть данные
updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
} else {
if (uiElements.player.panel) uiElements.player.panel.style.opacity = '0.5'; // Затемняем, если нет данных
}
// Обновление панели "моего оппонента" ( персонажа в слоте противника для этого клиента)
const opponentStateInGameState = currentGameState[opponentActualSlotId];
const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; // opponentBaseStats в gameData - это всегда статы оппонента этого клиента
if (opponentStateInGameState && opponentBaseStatsForUI) {
// Если игра не окончена, а панель оппонента "тает" или не полностью видна, восстанавливаем это
if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || uiElements.opponent.panel.classList.contains('dissolving')) && currentGameState.isGameOver === false ) {
console.log("[UI UPDATE DEBUG] Opponent panel not fully visible/dissolving but game not over. Restoring opacity/transform.");
const panel = uiElements.opponent.panel;
panel.classList.remove('dissolving');
// Force reflow before applying instant style change
panel.offsetHeight; // Trigger reflow
panel.style.opacity = '1';
panel.style.transform = 'scale(1) translateY(0)';
} else if (uiElements.opponent.panel) {
uiElements.opponent.panel.style.opacity = '1'; // Убеждаемся, что видна, если есть данные
}
updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
} else {
// Нет данных оппонента ( например, PvP игра ожидает игрока). Затемняем панель.
if (uiElements.opponent.panel) {
uiElements.opponent.panel.style.opacity = '0.5';
// Очищаем панель, если данных нет
if(uiElements.opponent.name) uiElements.opponent.name.innerHTML = '<i class="fas fa-question icon-opponent"></i> Ожидание игрока...';
if(uiElements.opponent.hpText) uiElements.opponent.hpText.textContent = 'N/A';
if(uiElements.opponent.resourceText) uiElements.opponent.resourceText.textContent = 'N/A';
if(uiElements.opponent.status) uiElements.opponent.status.textContent = 'Не готов';
if(uiElements.opponent.buffsList) uiElements.opponent.buffsList.innerHTML = 'Нет';
if(uiElements.opponent.debuffsList) uiElements.opponent.debuffsList.innerHTML = 'Нет';
if(uiElements.opponent.avatar) uiElements.opponent.avatar.src = 'images/default_avatar.png';
if(uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question';
if(uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
}
}
updateEffectsUI(currentGameState);
// Обновление заголовка игры ( Имя1 vs Имя2)
if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
const myName = gameDataGlobal.playerBaseStats.name; // Имя моего персонажа
const opponentName = gameDataGlobal.opponentBaseStats.name; // Имя моего оппонента
const myKey = gameDataGlobal.playerBaseStats.characterKey;
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
let myClass = 'title-player'; // Базовый класс
if (myKey === 'elena') myClass = 'title-enchantress';
else if (myKey === 'almagest') myClass = 'title-sorceress';
let opponentClass = 'title-opponent'; // Базовый класс
if (opponentKey === 'elena') opponentClass = 'title-enchantress';
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
else if (opponentKey === 'balard') opponentClass = 'title-knight';
// Используем имена персонажей, которые видит этот клиент
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
}
// Управление активностью кнопок
// Этот клиент может действовать только если его технический слот ('player' или 'opponent')
// соответствует слоту, чей сейчас ход ( actorSlotWhoseTurnItIs).
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
const isGameActive = !currentGameState.isGameOver;
// Кнопка атаки
if (uiElements.controls.buttonAttack) {
// Кнопка атаки активна, если это ход этого клиента и игра активна
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
// Управление классом для подсветки бафнутой атаки
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
const myStateForAttackBuff = currentGameState[myActualPlayerId]; // Состояние моего персонажа
let attackBuffId = null;
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
if (attackBuffId && myStateForAttackBuff && myStateForAttackBuff.activeEffects) {
// Проверяем, есть ли активный бафф атаки И он не только что наложен в этом ходу
const isAttackBuffReady = myStateForAttackBuff.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast && eff.turnsLeft > 0);
// Подсветка активна, если бафф готов И это ход этого клиента И игра активна
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive);
} else {
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
}
}
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; // Пока не используется
// Кнопки способностей
const actingPlayerState = currentGameState[myActualPlayerId]; // Состояние моего персонажа
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа (с точки зрения клиента)
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName; // Имя ресурса моего персонажа
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName || !isGameActive) {
// Если нет необходимых данных или игра неактивна, дизейблим все кнопки способностей
if(button instanceof HTMLButtonElement) button.disabled = true;
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
return;
}
const abilityId = button.dataset.abilityId;
const ability = actingPlayerAbilities.find(ab => ab.id === abilityId);
if (!ability) {
button.disabled = true; // Если способность не найдена в данных (ошибка)
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
return;
}
// Проверяем условия доступности способности
const hasEnoughResource = actingPlayerState.currentResource >= ability.cost;
// Бафф уже активен (для баффов, которые не стакаются)
const isBuffAlreadyActive = ability.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects?.some(eff => eff.id === ability.id);
// На общем кулдауне
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[ability.id] || 0) > 0;
// Под полным безмолвием
const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
// Под специфическим заглушением этой способности
const specificSilenceEffect = actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
const isSpecificallySilenced = !!specificSilenceEffect;
const isSilenced = isGenerallySilenced || isSpecificallySilenced; // Считается заглушенным, если под полным или специфическим безмолвием
// Определяем длительность безмолвия для отображения (берем из специфического, если есть, иначе из полного)
const silenceTurnsLeft = isSpecificallySilenced
? (specificSilenceEffect?.turnsLeft || 0)
: (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0);
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
// Нужна проверка состояния ОППОНЕНТА этого клиента (т.е. цели)
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
let isDebuffAlreadyOnTarget = false;
const isTargetedDebuffAbility = ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF;
if (isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects) {
const effectIdForDebuff = 'effect_' + ability.id; // Ищем эффект с префиксом effect_ на цели
isDebuffAlreadyOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff);
}
// Кнопка активна, если:
// - Это ход этого клиента
// - Игра активна
// - Достаточно ресурса
// - Бафф не активен (если это бафф)
// - Не на кулдауне
// - Не под безмолвием (полным или специфическим)
// - Дебафф не активен на цели (если это такой дебафф)
button.disabled = !(canThisClientAct && isGameActive) ||
!hasEnoughResource ||
isBuffAlreadyActive ||
isSilenced ||
isOnCooldown ||
isDebuffAlreadyOnTarget;
// Управление классами для стилизации кнопки
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
if (isOnCooldown) {
button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[ability.id]}`; cooldownDisplay.style.display = 'block'; }
} else if (isSilenced) {
button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
if (cooldownDisplay) {
const icon = isGenerallySilenced ? '🔕' : '🔇'; // Иконка для полного/частичного безмолвия
cooldownDisplay.textContent = `${icon} ${silenceTurnsLeft}`;
cooldownDisplay.style.display = 'block';
}
} else {
if (cooldownDisplay) cooldownDisplay.style.display = 'none'; // Скрываем, если нет ни КД, ни безмолвия
// Добавляем классы, если действие возможно, но есть ограничения (недостаточно ресурса, бафф активен, дебафф на цели)
// Эти классы используются для визуальной обратной связи, когда кнопка *не* задизейблена по КД или безмолвию.
// Если кнопка disabled из-за !hasEnoughResource, классы not-enough-resource и buff-is-active все равно могут быть добавлены.
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource && !isBuffAlreadyActive && !isDebuffAlreadyOnTarget);
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDebuffAlreadyOnTarget);
// Если дебафф уже на цели, но кнопка не задизейблена по другим причинам, можно добавить отдельный класс
// button.classList.toggle('debuff-on-target', isDebuffAlreadyOnTarget && !button.disabled);
}
// Обновление title (всплывающей подсказки) - показываем полную информацию
let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`;
let descriptionTextFull = ability.description; // Используем описание, пришедшее с сервера
if (descriptionTextFull) titleText += ` - ${descriptionTextFull}`;
let abilityBaseCooldown = ability.cooldown; // Исходный КД из данных
// Учитываем внутренние КД Баларда, если это способность Баларда (хотя игроку AI способности не отображаются, но для полноты)
// if (actingPlayerState.characterKey === 'balard') {
// if (ability.id === configGlobal.ABILITY_ID_BALARD_SILENCE) abilityBaseCooldown = configGlobal.BALARD_SILENCE_INTERNAL_COOLDOWN;
// else if (ability.id === configGlobal.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') abilityBaseCooldown = ability.internalCooldownValue;
// }
if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) {
titleText += ` (КД: ${abilityBaseCooldown} х.)`;
}
// Добавляем информацию о текущем состоянии (КД, безмолвие, активный бафф/debuff) в тултип, если применимо
if (isOnCooldown) {
titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`;
}
if (isSilenced) {
titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
}
if (isBuffAlreadyActive) {
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}.`;
}
if (isDebuffAlreadyOnTarget && opponentStateForDebuffCheck) {
const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
titleText += ` | Эффект уже наложен на ${opponentBaseStatsForUI?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
}
if (!hasEnoughResource) {
titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${ability.cost})`;
}
button.setAttribute('title', titleText);
});
}
/**
* Показывает модальное окно конца игры.
* @param {boolean} playerWon - Флаг, выиграл ли игрок, управляющий этим клиентом.
* @param {string} [reason=""] - Причина завершения игры.
* @param {string|null} opponentCharacterKeyFromClient - Ключ персонажа оппонента с т.з. клиента.
* @param {object} [data=null] - Полный объект данных из события gameOver (включает disconnectedCharacterName и т.д.)
*/
function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) { // ИСПРАВЛЕНО: Добавлен аргумент data
const config = window.GAME_CONFIG || {};
// Используем gameData, сохраненное в client.js, так как оно отражает перспективу этого клиента
const clientSpecificGameData = window.gameData;
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
const gameOverScreenElement = uiElements.gameOver.screen;
console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`);
console.log(`[UI.JS DEBUG] Current window.gameState.isGameOver: ${currentActualGameState?.isGameOver}`);
console.log(`[UI.JS DEBUG] Opponent Character Key (from client via param): ${opponentCharacterKeyFromClient}`);
console.log(`[UI.JS DEBUG] My Character Name (from window.gameData): ${clientSpecificGameData?.playerBaseStats?.name}`);
console.log(`[UI.JS DEBUG] Opponent Character Name (from window.gameData): ${clientSpecificGameData?.opponentBaseStats?.name}`);
console.log(`[UI.JS DEBUG] Full game over data received:`, data); // Добавьте этот лог
if (!gameOverScreenElement) {
console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found.");
return;
}
const resultMsgElement = uiElements.gameOver.message;
// Имена для сообщения берем из clientSpecificGameData, т.к. они уже "перевернуты" для каждого клиента
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
if (resultMsgElement) {
let winText = `Победа! ${myNameForResult} празднует!`;
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
if (reason === 'opponent_disconnected') {
// Определяем, кто отключился, по данным из события gameOver
let disconnectedName = "Противник";
if (data && data.disconnectedCharacterName) {
disconnectedName = data.disconnectedCharacterName;
} else {
// Фоллбэк на имя оппонента с точки зрения клиента
disconnectedName = opponentNameForResult;
}
winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
// Если оппонент отключился, а мы проиграли (технически такое возможно, если сервер так решит)
// То текст поражения можно оставить стандартным или специфичным.
// Пока оставим стандартный, если playerWon = false и reason = opponent_disconnected.
}
resultMsgElement.textContent = playerWon ? winText : loseText;
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
}
const opponentPanelElement = uiElements.opponent.panel;
if (opponentPanelElement) {
// Сначала убеждаемся, что анимация растворения снята, если она была активна от предыдущей попытки
// console.log(`[UI.JS DEBUG] Opponent panel classList before potential dissolve: ${opponentPanelElement.className}`);
opponentPanelElement.classList.remove('dissolving');
opponentPanelElement.offsetHeight; // Trigger reflow to reset state instantly
// Используем opponentCharacterKeyFromClient, так как это ключ реального персонажа оппонента с т.з. клиента
const keyForDissolveEffect = opponentCharacterKeyFromClient;
// Применяем анимацию растворения только если игра окончена, игрок выиграл, и это не дисконнект,
// и противник был Балардом или Альмагест (у которых есть эта анимация).
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon && reason !== 'opponent_disconnected') {
if (keyForDissolveEffect === 'balard' || keyForDissolveEffect === 'almagest') {
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel. Conditions met.`);
opponentPanelElement.classList.add('dissolving');
// Убеждаемся, что панель станет полностью прозрачной и сместится после анимации
opponentPanelElement.style.opacity = '0';
// opponentPanelElement.style.transform = 'scale(0.9) translateY(20px)'; // Трансформация уже в CSS анимации
} else {
console.log(`[UI.JS DEBUG] NOT adding .dissolving (opponent key mismatch for dissolve effect: ${keyForDissolveEffect} or reason: ${reason}).`);
// Если анимация не применяется, убеждаемся, что панель видна
opponentPanelElement.style.opacity = '1';
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
}
} else {
console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}, reason=${reason}.`);
// Если игра не окончена или игрок проиграл/оппонент отключился, убеждаемся, что панель видна
opponentPanelElement.style.opacity = '1';
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
}
// console.log(`[UI.JS DEBUG] Opponent panel classList FINAL in showGameOver (before timeout): ${opponentPanelElement.className}`);
}
// Показываем модальное окно конца игры с небольшой задержкой
// ИСПРАВЛЕНО: Передаем аргументы в колбэк, чтобы не полагаться на глобальный gameState
setTimeout((finalState, won, gameOverReason, opponentKeyForModalParam, eventData) => { // Названия аргументов, чтобы избежать путаницы
console.log("[UI.JS DEBUG] Timeout callback fired.");
console.log("[UI.JS DEBUG] State in timeout:", finalState);
console.log("[UI.JS DEBUG] isGameOver in state:", finalState?.isGameOver);
// Перепроверяем состояние перед показом на случай быстрых обновлений
if (finalState && finalState.isGameOver === true) { // Используем переданный state
console.log(`[UI.JS DEBUG] Condition (finalState && finalState.isGameOver === true) IS TRUE. Attempting to show modal.`);
// Убеждаемся, что modal не имеет display: none перед запуском transition opacity
// display: none полностью убирает элемент из потока и не позволяет анимировать opacity
// Переводим display в flex, если он был hidden (display: none !important в CSS)
if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
}
// Убеждаемся, что opacity 0 для начала анимации
gameOverScreenElement.style.opacity = '0';
// Убеждаемся, что display корректен
gameOverScreenElement.style.display = 'flex'; // Или какой там display в CSS для .modal
requestAnimationFrame(() => {
// Применяем opacity 1 после display flex для анимации
gameOverScreenElement.style.opacity = '1';
// Запускаем анимацию контента модального окна (scale/translate)
if (uiElements.gameOver.modalContent) {
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
uiElements.gameOver.modalContent.style.opacity = '1';
}
});
} else {
console.log(`[UI.JS DEBUG] Condition (finalState && finalState.isGameOver === true) IS FALSE. Modal will NOT be shown.`);
// Убеждаемся, что модалка скрыта, если условия больше не выполняются
gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
gameOverScreenElement.style.opacity = '0'; // Убеждаемся, что opacity сброшен
}
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); // ИСПРАВЛЕНО: Передаем аргументы
}
// Экспортируем функции UI для использования в client.js
window.gameUI = { uiElements, addToLog, updateUI, showGameOver };
})();