// /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 = ` ${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 `${displayText}`; }).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 = ' Ожидание игрока...'; 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 = `${myName} ${opponentName}`; } // Управление активностью кнопок // Этот клиент может действовать только если его технический слот ('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 }; })();