bc/public/js/ui.js

826 lines
65 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'),
returnToMenuButton: document.getElementById('return-to-menu-button'),
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) {
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
if (elements && elements.panel && elements.panel.style.display !== 'none') {
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
// elements.panel.style.opacity = '0.5'; // Пример: сделать полупрозрачной, если нет данных
}
// ВАЖНО: Очистить содержимое панели, если данных нет.
if (elements) {
if(elements.name) elements.name.innerHTML = (panelRole === 'player') ? '<i class="fas fa-question icon-player"></i> Ожидание данных...' : '<i class="fas fa-question icon-opponent"></i> Ожидание игрока...';
if(elements.hpText) elements.hpText.textContent = 'N/A';
if(elements.resourceText) elements.resourceText.textContent = 'N/A';
if(elements.status) elements.status.textContent = 'Неизвестно';
if(elements.buffsList) elements.buffsList.innerHTML = 'Нет';
if(elements.debuffsList) elements.debuffsList.innerHTML = 'Нет';
if(elements.avatar) elements.avatar.src = 'images/default_avatar.png';
if(panelRole === 'player' && uiElements.playerResourceTypeIcon) uiElements.playerResourceTypeIcon.className = 'fas fa-question';
if(panelRole === 'opponent' && uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question';
if(panelRole === 'player' && uiElements.playerResourceBarContainer) uiElements.playerResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
if(panelRole === 'opponent' && uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
if(elements.panel) elements.panel.style.opacity = '0.5'; // Затемняем
}
return;
}
if (elements.panel) elements.panel.style.opacity = '1'; // Делаем видимой, если данные есть
// Обновление имени и иконки персонажа
if (elements.name) {
let iconClass = 'fa-question'; // Иконка по умолчанию
const characterKey = fighterBaseStats.characterKey;
// Определяем класс иконки в зависимости от персонажа
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; }
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; }
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; }
else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки имени.`); */ }
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
if (isControlledByThisClient) nameHtml += " (Вы)";
elements.name.innerHTML = nameHtml;
}
// Обновление аватара
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'; } // или fa-wand-magic-sparkles, fa-star-half-alt и т.д.
else { console.warn(`updateFighterPanelUI: Unknown resource name "${fighterBaseStats.resourceName}" for icon/color.`); iconClass = 'fa-question-circle'; }
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)'; }
else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; }
else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; }
else { console.warn(`updateFighterPanelUI: Unknown character key "${fighterBaseStats.characterKey}" for panel border color.`); }
let glowColorVar = 'rgba(0, 0, 0, 0.4)'; // Базовая тень
if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
// В твоем CSS --panel-glow-opponent используется для обоих Баларда и Альмагест
else if (fighterBaseStats.characterKey === 'almagest' || fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)';
elements.panel.style.borderColor = borderColorVar;
elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
}
}
/**
* Генерирует HTML для списка эффектов.
* @param {Array<object>} effectsArray - Массив объектов эффектов, УЖЕ отфильтрованных и отсортированных.
* @returns {string} HTML-строка для отображения списка эффектов.
*/
function generateEffectsHTML(effectsArray) {
const config = window.GAME_CONFIG || {};
if (!effectsArray || effectsArray.length === 0) return 'Нет';
// ВАЖНО: Сортировка теперь выполняется ВНЕ этой функции (в updateEffectsUI)
return effectsArray.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) {
// Эффекты полного безмолвия, заглушения абилок или типа DISABLE
effectClasses += ' effect-stun'; // Класс для стана/безмолвия (красный/желтый)
} else if (eff.grantsBlock) { // Эффекты, дающие блок
effectClasses += ' effect-block'; // Класс для эффектов блока (синий)
} else if (eff.type === config.ACTION_TYPE_DEBUFF) { // Явные дебаффы (например, сжигание ресурса)
effectClasses += ' effect-debuff'; // Класс для ослаблений (красноватый)
} else if (eff.type === config.ACTION_TYPE_BUFF) { // Явные баффы (например, усиление атаки)
effectClasses += ' effect-buff'; // Класс для усилений (зеленый)
} else if (eff.type === config.ACTION_TYPE_HEAL) { // Эффекты лечения (HoT)
effectClasses += ' effect-buff'; // HoT стилизуем как бафф (зеленый)
}
// Если есть другие типы (DoT, Drain и т.п.), которые не входят в эти категории,
// их нужно добавить или стилизовать как info.
// DoT можно стилизовать как effect-debuff or effect-damage, Drain as effect-debuff.
// Например: else if (eff.type === config.ACTION_TYPE_DAMAGE) { effectClasses += ' effect-debuff'; } // DoT как дебафф
// else if (eff.type === config.ACTION_TYPE_DRAIN) { effectClasses += ' effect-debuff'; } // Drain как дебафф
else {
//console.warn(`generateEffectsHTML: Эффект ID "${eff.id}" с типом "${eff.type}" не имеет специфичного класса стилизации.`);
effectClasses += ' effect-info'; // Класс по умолчанию или информационный (серый/синий)
}
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
}).join(' ');
}
function updateEffectsUI(currentGameState) {
if (!currentGameState || !window.GAME_CONFIG) { return; }
const mySlotId = window.myPlayerId;
const config = window.GAME_CONFIG;
if (!mySlotId) { return; }
const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : config.PLAYER_ID;
const myState = currentGameState[mySlotId];
const opponentState = currentGameState[opponentSlotId];
// --- Логика сортировки эффектов (для использования как для баффов, так и для дебаффов) ---
// Сортируем эффекты по типу: сначала позитивные, потом негативные, потом контроля
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
// Добавьте другие типы, если нужно сортировать
};
const sortEffects = (a, b) => {
// Определяем порядок для эффекта A
let orderA = typeOrder[a.type] || 99;
if (a.grantsBlock) orderA = typeOrder.grantsBlock;
// isFullSilence и playerSilencedOn_X - это эффекты типа DISABLE, но их можно поставить выше в приоритете дебаффов
if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
// Добавьте сюда другие специфичные проверки, если нужно изменить стандартный порядок по типу
// Определяем порядок для эффекта B
let orderB = typeOrder[b.type] || 99;
if (b.grantsBlock) orderB = typeOrder.grantsBlock;
if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
return (orderA || 99) - (orderB || 99); // Сортируем по порядку
};
// --- Конец логики сортировки ---
// --- Обработка эффектов Игрока (My Player) ---
if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
const myBuffs = [];
const myDebuffs = [];
// ИСПРАВЛЕНО: Проходим по массиву activeEffects один раз и пушим в нужный список
myState.activeEffects.forEach(e => {
// Определяем, является ли эффект баффом
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; // HoT как бафф
// Определяем, является ли эффект дебаффом
// Учитываем типы DEBUFF, DISABLE, а также специфические флаги/ID для полного безмолвия и заглушения конкретных абилок
const isDebuff = e.type === config.ACTION_TYPE_DEBUFF || e.type === config.ACTION_TYPE_DISABLE || e.isFullSilence || e.id.startsWith('playerSilencedOn_');
// Добавляем эффект в соответствующий список (каждый эффект должен попасть только в один)
if (isBuff) {
myBuffs.push(e);
} else if (isDebuff) {
myDebuffs.push(e);
} else {
// Если эффект не попал ни в одну категорию (например, новый тип?)
//console.warn(`updateEffectsUI: Эффект ID "${e.id}" с типом "${e.type}" не отнесен ни к баффам, ни к дебаффам для Игрока.`);
myDebuffs.push(e); // Добавим в дебаффы по умолчанию
}
});
// Сортируем списки баффов и дебаффов перед генерацией HTML
myBuffs.sort(sortEffects);
myDebuffs.sort(sortEffects);
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 = 'Нет';
}
// --- Обработка эффектов Оппонента (Opponent Player) ---
// Логика аналогична игроку, но условия дебаффов могут немного отличаться
// (например, префикс ID заглушения абилок)
if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
const opponentBuffs = [];
const opponentDebuffs = [];
// ИСПРАВЛЕНО: Проходим по массиву activeEffects оппонента один раз и пушим в нужный список
opponentState.activeEffects.forEach(e => {
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; // HoT как бафф
// Определяем, является ли эффект дебаффом для ОППОНЕНТА
// Учитываем типы DEBUFF, DISABLE, isFullSilence.
// id.startsWith('playerSilencedOn_') специфично для игрока,
// id.startsWith('effect_') используется для дебаффов, наложенных на цель (например, Seal of Weakness)
const isDebuff = e.type === config.ACTION_TYPE_DEBUFF || e.type === config.ACTION_TYPE_DISABLE || e.isFullSilence || e.id.startsWith('effect_');
// Если у оппонента есть свои специфичные эффекты заглушения с другим префиксом, его тоже нужно добавить сюда.
if (isBuff) {
opponentBuffs.push(e);
} else if (isDebuff) {
opponentDebuffs.push(e);
} else {
//console.warn(`updateEffectsUI: Эффект ID "${e.id}" с типом "${e.type}" не отнесен ни к баффам, ни к дебаффам для Оппонента.`);
opponentDebuffs.push(e); // Добавим в дебаффы по умолчанию
}
});
// Сортируем списки баффов и дебаффов оппонента
opponentBuffs.sort(sortEffects);
opponentDebuffs.sort(sortEffects);
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.");
// Сбрасываем UI панелей, если данные отсутствуют
updateFighterPanelUI('player', null, null, true);
updateFighterPanelUI('opponent', null, null, false);
// Скрываем/очищаем остальные элементы UI игры
if(uiElements.gameHeaderTitle) uiElements.gameHeaderTitle.innerHTML = `<span>Ожидание данных...</span>`;
if(uiElements.controls.turnIndicator) uiElements.controls.turnIndicator.textContent = "Ожидание данных...";
if(uiElements.controls.buttonAttack) uiElements.controls.buttonAttack.disabled = true;
if(uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
if(uiElements.controls.abilitiesGrid) uiElements.controls.abilitiesGrid.innerHTML = '<p class="placeholder-text">Загрузка способностей...</p>';
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) {
updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
} else {
updateFighterPanelUI('player', null, null, true); // Нет данных, показываем состояние ожидания
}
// Обновление панели "моего оппонента"
const opponentStateInGameState = currentGameState[opponentActualSlotId];
const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; // opponentBaseStats в gameData - это всегда статы оппонента этого клиента
// Если игра окончена и игрок победил, возможно, панель оппонента уже анимирована на исчезновение.
// Не сбрасываем ее opacity/transform здесь, если она в состоянии dissolving.
const isOpponentPanelDissolving = uiElements.opponent.panel?.classList.contains('dissolving');
if (opponentStateInGameState && opponentBaseStatsForUI) {
// Если игра не окончена, а панель оппонента "тает" или не полностью видна, восстанавливаем это
// Но не если она активно в анимации растворения (dissolving)
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;
if (panel.classList.contains('dissolving')) {
panel.classList.remove('dissolving');
panel.style.transition = 'none'; // Отключаем переход временно
panel.offsetHeight; // Trigger reflow
panel.style.opacity = '1';
panel.style.transform = 'scale(1) translateY(0)';
panel.style.transition = ''; // Восстанавливаем переход
} else {
panel.style.opacity = '1';
panel.style.transform = 'scale(1) translateY(0)'; // В случае если просто opacity < 1
}
} else if (uiElements.opponent.panel && !isOpponentPanelDissolving) {
uiElements.opponent.panel.style.opacity = '1'; // Убеждаемся, что видна, если есть данные и не растворяется
}
updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
} else {
// Нет данных оппонента ( например, PvP игра ожидает игрока). Затемняем панель и очищаем.
// Но не сбрасываем opacity/transform, если она активно в анимации растворения
if (!isOpponentPanelDissolving) {
updateFighterPanelUI('opponent', null, null, false); // Нет данных, показываем состояние ожидания/пустоты
} else {
// Если панель растворяется, не обновляем ее содержимое и оставляем текущие стили opacity/transform
console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update.");
}
}
// Обновление эффектов
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';
else if (myKey === 'balard') myClass = 'title-knight'; // Вдруг AI Балард в PvP
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>`;
} else if (uiElements.gameHeaderTitle) {
// Обновление заголовка в режиме ожидания
const myName = gameDataGlobal.playerBaseStats?.name || 'Игрок 1';
const myKey = gameDataGlobal.playerBaseStats?.characterKey;
let myClass = 'title-player';
if (myKey === 'elena') myClass = 'title-enchantress';
else if (myKey === 'almagest') myClass = 'title-sorceress';
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Ожидание игрока...</span>`;
}
// Управление активностью кнопок и индикатор хода
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
const isGameActive = !currentGameState.isGameOver;
const myCharacterState = currentGameState[myActualPlayerId];
// Обновление индикатора хода
if (uiElements.controls.turnIndicator) {
if (isGameActive) {
const currentTurnActor = currentGameState.isPlayerTurn ? currentGameState.player : currentGameState.opponent;
uiElements.controls.turnIndicator.textContent = `Ход ${currentGameState.turnNumber}: ${currentTurnActor?.name || 'Неизвестно'}`;
// Управляем цветом индикатора хода
if (currentTurnActor?.id === myActualPlayerId) {
uiElements.controls.turnIndicator.style.color = 'var(--turn-color)'; // Свой ход - желтый
} else {
uiElements.controls.turnIndicator.style.color = 'var(--text-muted)'; // Ход противника - приглушенный
}
} else {
uiElements.controls.turnIndicator.textContent = "Игра окончена"; // Или можно скрыть его
uiElements.controls.turnIndicator.style.color = 'var(--text-muted)';
}
}
// Кнопка атаки
if (uiElements.controls.buttonAttack) {
// Кнопка атаки активна, если это ход этого клиента и игра активна (полное безмолвие не блокирует базовую атаку)
// ИСПРАВЛЕНО: Убрана проверка !isFullySilenced из условия disabled для базовой атаки
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
// Управление классом для подсветки бафнутой атаки
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
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 && myCharacterState && myCharacterState.activeEffects) {
// Проверяем, есть ли активный "отложенный" бафф (isDelayed=true) на атакующем,
// который готов сработать на следующую атаку.
const isAttackBuffReady = myCharacterState.activeEffects.some(
eff => (eff.id === attackBuffId || eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK)
&& eff.isDelayed // Явно проверяем, что это отложенный бафф
&& eff.turnsLeft > 0 // Эффект должен еще действовать
&& !eff.justCast // Не должен быть наложен в этом ходу, чтобы сработать НА ЭТОМ ходу
);
// Подсветка активна, если бафф готов И это ход этого клиента И игра активна
// Подсветка не зависит от безмолвия, т.к. атака возможна и под безмолвием.
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 = myCharacterState; // Состояние моего персонажа
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа (с точки зрения клиента)
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName; // Имя ресурса моего персонажа
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; // Состояние оппонента этого клиента
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
// Получаем актуальное состояние способности из actingPlayerState (которое пришло с сервера)
const abilityId = button.dataset.abilityId;
const abilityDataFromGameData = actingPlayerAbilities?.find(ab => ab.id === abilityId);
// Если игра неактивна, нет данных о бойце, способностях или ресурсе, дизейблим кнопку.
if (!(button instanceof HTMLButtonElement) || !isGameActive || !canThisClientAct || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName || !abilityDataFromGameData) {
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; // Пропускаем дальнейшую логику обновления кнопки, если она должна быть disabled по базовым условиям
}
// Проверяем условия доступности способности из актуального состояния игры (actingPlayerState)
const hasEnoughResource = actingPlayerState.currentResource >= abilityDataFromGameData.cost;
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0; // Проверяем КД по ID способности из актуального состояния
// Под полным безмолвием
const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
// Под специфическим заглушением этой способности (ищем в disabledAbilities актуального состояния)
const isAbilitySpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
const isSilenced = isGenerallySilenced || isAbilitySpecificallySilenced; // Считается заглушенным, если под полным или специфическим безмолвием
// Определяем длительность безмолвия для отображения (берем из специфического, если есть, иначе из полного)
const silenceTurnsLeft = isAbilitySpecificallySilenced
? (actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0)
: (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0);
// Нельзя кастовать бафф, если он уже активен (для баффов, которые не стакаются)
const isBuffAlreadyActive = abilityDataFromGameData.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects?.some(eff => eff.id === abilityId);
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
const isTargetedDebuffAbility = abilityId === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || abilityId === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF;
const effectIdForDebuff = 'effect_' + abilityId; // Ищем эффект с префиксом effect_ на цели (оппоненте)
const isDebuffAlreadyOnTarget = isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects?.some(e => e.id === effectIdForDebuff);
// Кнопка способности активна, если:
// - Это ход этого клиента (проверено выше: canThisClientAct)
// - Игра активна (проверено выше: isGameActive)
// - Достаточно ресурса
// - Бафф не активен (если это бафф)
// - Не на кулдауне
// - Не под безмолвием (полным или специфическим) <--- ЭТО УСЛОВИЕ ОСТАЕТСЯ ДЛЯ СПОСОБНОСТЕЙ
// - Дебафф не активен на цели (если это такой дебафф)
button.disabled = !hasEnoughResource ||
isBuffAlreadyActive ||
isSilenced || // Способности БЛОКИРУЮТСЯ полным безмолвием
isOnCooldown ||
isDebuffAlreadyOnTarget;
// Управление классами для стилизации кнопки (применяются независимо от окончательного disabled состояния)
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[abilityId]}`; 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)
if (!isOnCooldown && !isSilenced) {
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource);
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive);
// Если дебафф уже на цели, но кнопка не задизейблена по другим причинам, можно добавить отдельный класс для стилизации
// button.classList.toggle('debuff-on-target', isDebuffAlreadyOnTarget);
}
}
// Обновление title (всплывающей подсказки) - показываем полную информацию
// Используем abilityDataFromGameData для базовой информации
let titleText = `${abilityDataFromGameData.name} (${abilityDataFromGameData.cost} ${actingPlayerResourceName})`;
let descriptionTextFull = abilityDataFromGameData.description; // Используем описание, пришедшее с сервера
// Если есть функция описания, используем ее
if (typeof abilityDataFromGameData.descriptionFunction === 'function') {
// Передаем конфиг и статы оппонента (цели) для генерации описания
const opponentBaseStatsForDesc = gameDataGlobal.opponentBaseStats; // Статы оппонента этого клиента
descriptionTextFull = abilityDataFromGameData.descriptionFunction(configGlobal, opponentBaseStatsForDesc);
}
if (descriptionTextFull) titleText += ` - ${descriptionTextFull}`;
// Добавляем информацию об исходном КД из данных способности
let abilityBaseCooldown = abilityDataFromGameData.cooldown;
if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) {
titleText += ` (Исходный КД: ${abilityBaseCooldown} х.)`;
}
// Добавляем информацию о текущем состоянии (КД, безмолвие, активный бафф/debuff) в тултип, если применимо
if (isOnCooldown) {
titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`;
}
if (isSilenced) {
titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
}
if (isBuffAlreadyActive) {
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId); // Ищем активный эффект по ID способности
// Если бафф имеет свойство 'justCast' и наложен в этом ходу, он не "готов" сработать на ЭТОМ ходу.
// Это может быть важно для тултипа, если нужно отличать "только что наложен" от "готов к следующему действию".
// Для "Силы Природы" (isDelayed=true) состояние "активен" означает "готов сработать на следующую атаку".
const isDelayedBuffReady = isBuffAlreadyActive && activeEffect && activeEffect.isDelayed && !activeEffect.justCast && activeEffect.turnsLeft > 0;
if (isDelayedBuffReady) {
titleText += ` | Эффект активен и сработает при следующей базовой атаке (${activeEffect.turnsLeft} х.)`;
} else if (isBuffAlreadyActive) {
titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}. Нельзя применить повторно.`;
}
}
if (isDebuffAlreadyOnTarget && opponentStateForDebuffCheck) {
const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
titleText += ` | Эффект уже наложен на ${gameDataGlobal.opponentBaseStats?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
}
if (!hasEnoughResource) {
titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${abilityDataFromGameData.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) {
const config = window.GAME_CONFIG || {};
const clientSpecificGameData = window.gameData; // Используем gameData, сохраненное в client.js
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] captured currentActualGameState?.isGameOver at call time: ${currentActualGameState?.isGameOver}`); // Log state at call time
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;
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
if (resultMsgElement) {
let winText = `Победа! ${myNameForResult} празднует!`;
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
if (reason === 'opponent_disconnected') {
let disconnectedName = "Противник";
// Если в данных gameOver есть имя отключившегося персонажа, используем его
if (data && data.disconnectedCharacterName) {
disconnectedName = data.disconnectedCharacterName;
} else {
// Фоллбэк на имя оппонента с точки зрения клиента
disconnectedName = opponentNameForResult;
}
winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
// В PvP, если оппонент отключился, а текущий игрок проиграл (что странно, но возможно),
// сообщение о поражении может быть стандартным или специфичным.
// В AI режиме, если игрок отключился, нет формального победителя AI.
// Пусть будет стандартное поражение, если playerWon === false
if (!playerWon) {
// Возможно, специфичный текст для дисконнекта, когда ты проиграл?
// loseText = `Игра завершена из-за отключения ${disconnectedName}. Вы проиграли.`
}
} else if (reason === 'hp_zero') {
// Стандартное завершение по HP - тексты определены выше
}
// Добавьте обработку других причин завершения, если они будут
else {
// Неизвестная причина завершения
winText = `Игра окончена. Победа! (${reason})`;
loseText = `Игра окончена. Поражение. (${reason})`;
}
resultMsgElement.textContent = playerWon ? winText : loseText;
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
}
const opponentPanelElement = uiElements.opponent.panel;
if (opponentPanelElement) {
// Сначала убеждаемся, что анимация растворения снята, если она была активна от предыдущей попытки
// и не должна применяться сейчас.
opponentPanelElement.classList.remove('dissolving');
opponentPanelElement.style.transition = 'none'; // Временно отключаем transition
opponentPanelElement.offsetHeight; // Trigger reflow to apply style instantly
// Используем characterKey проигравшего (переданный из GameInstance),
// так как анимация растворения должна быть специфична для проигравшего персонажа,
// который может быть Балардом или Альмагест.
const loserCharacterKeyForDissolve = data?.loserCharacterKey;
// Применяем анимацию растворения только если игра окончена, игрок победил,
// и проигравший был Балардом или Альмагест (у которых есть эта анимация).
// Исключаем случай дисконнекта, если анимация должна быть только при "убийстве" по HP.
// В текущем CSS анимация растворения не зависит от причины, но зависит от класса 'dissolving'.
// Добавляем класс, если игра окончена, игрок победил, и проигравший персонаж - Балард или Альмагест.
// Если игра окончена И игрок проиграл И оппонент был Балардом/Альмагест, но игрок проиграл, анимация растворения НЕ применяется к панели оппонента.
// Поэтому условие playerWon && ... корректно.
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon) {
// Проверяем, является ли проигравший (т.е. оппонент этого клиента) Балардом или Альмагест
if (loserCharacterKeyForDissolve === 'balard' || loserCharacterKeyForDissolve === 'almagest') {
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel.`);
opponentPanelElement.classList.add('dissolving');
// Убеждаемся, что панель станет полностью прозрачной и сместится после анимации.
// Конечные стили (opacity: 0, transform) могут быть заданы в CSS для класса .dissolving,
// но их можно также установить здесь после добавления класса для гарантии.
opponentPanelElement.style.opacity = '0'; // Конечный стиль для transition
// opponentPanelElement.style.transform = 'scale(0.9) translateY(20px)'; // Конечный стиль для transition, если нужен
} else {
console.log(`[UI.JS DEBUG] NOT adding .dissolving (loser key mismatch: ${loserCharacterKeyForDissolve}).`);
// Если анимация не применяется, убеждаемся, что панель полностью видна
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}.`);
// Если игра не окончена или игрок проиграл, убеждаемся, что панель полностью видна
opponentPanelElement.style.opacity = '1';
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
}
opponentPanelElement.style.transition = ''; // Восстанавливаем transition после установки начальных/конечных стилей
}
// Показываем модальное окно конца игры с небольшой задержкой
// Передаем аргументы в колбэк, чтобы не полагаться на глобальный gameState в момент срабатывания setTimeout
setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => { // Use distinct names in timeout
console.log("[UI.JS DEBUG] Timeout callback fired for showGameOver.");
console.log("[UI.JS DEBUG] State object received in timeout:", finalStateInTimeout); // Check the whole object
console.log("[UI.JS DEBUG] isGameOver in state (TIMEOUT):", finalStateInTimeout?.isGameOver); // Check property
console.log("[UI.JS DEBUG] playerWon flag (TIMEOUT):", wonInTimeout); // Check playerWon flag passed
// Проверяем условия для показа модального окна: элемент существует И состояние игры помечено как оконченное
// ИСПРАВЛЕНО: Убрана проверка gameOverScreenElement.offsetParent !== null
if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) {
console.log(`[UI.JS DEBUG] Modal SHOW condition met: gameOverScreenElement exists, finalState exists, isGameOver is true.`);
// Убеждаемся, что modal не имеет display: none перед запуском transition opacity
if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
}
// Применяем display: flex (или другой нужный) только один раз, если нужно
if(window.getComputedStyle(gameOverScreenElement).display === 'none') {
gameOverScreenElement.style.display = 'flex'; // Или какой там display в CSS для .modal
}
gameOverScreenElement.style.opacity = '0'; // Start from hidden opacity
requestAnimationFrame(() => {
console.log("[UI.JS DEBUG] RequestAnimationFrame callback fired, animating modal.");
// Animate to visible
gameOverScreenElement.style.opacity = '1';
if (uiElements.gameOver.modalContent) {
uiElements.gameOver.modalContent.style.transition = 'transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out'; // Убеждаемся, что transition включен
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
uiElements.gameOver.modalContent.style.opacity = '1';
// uiElements.gameOver.modalContent.style.transition = ''; // Можно и так, если не отключали ранее
}
});
} else {
console.log(`[UI.JS DEBUG] Modal SHOW condition NOT met.`);
console.log(`[UI.JS DEBUG] Details: gameOverScreenElement=${!!gameOverScreenElement}, finalState=${!!finalStateInTimeout}, finalState?.isGameOver=${finalStateInTimeout?.isGameOver}. Hiding modal.`); // More details
// Убеждаемся, что модалка скрыта, если условия не выполняются
if (gameOverScreenElement) {
// Ensure transition is off when hiding instantly
gameOverScreenElement.style.transition = 'none';
if (uiElements.gameOver.modalContent) uiElements.gameOver.modalContent.style.transition = 'none';
gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
gameOverScreenElement.style.opacity = '0';
if (uiElements.gameOver.modalContent) {
uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
uiElements.gameOver.modalContent.style.opacity = '0';
}
// Trigger reflow to ensure transition is off before hiding
gameOverScreenElement.offsetHeight;
}
}
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); // Pass captured state and other values
}
// Экспортируем функции UI для использования в client.js
window.gameUI = { uiElements, addToLog, updateUI, showGameOver };
})();