// /public/js/ui.js // Этот файл отвечает за обновление DOM на основе состояния игры, // полученного от client.js (который, в свою очередь, получает его от сервера). // console.log("Loading ui.js..."); // GAME_CONFIG и gameData теперь предполагается, что будут установлены в window // из client.js после получения данных от сервера. // Это не лучшая практика (глобальные переменные), но для адаптации // существующего кода это может быть проще на первом этапе. // В идеале, все функции updateUI должны принимать gameState и gameData как параметры. (function() { // --- DOM Элементы (остаются как были) --- const uiElements = { player: { panel: document.getElementById('player-panel'), name: document.getElementById('player-name'), // Для обновления имени в PvP avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'), hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'), maxHpSpan: document.getElementById('player-max-hp'), resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'), maxResourceSpan: document.getElementById('player-max-resource'), 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'), // Для обновления имени в PvP avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'), hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'), maxHpSpan: document.getElementById('opponent-max-hp'), resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'), maxResourceSpan: document.getElementById('opponent-max-resource'), 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'), modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content') }, // Дополнительные элементы для PvP информации, если нужно gameHeaderTitle: document.querySelector('.game-header h1') // Заголовок "Елена vs Балард" }; /** Добавляет сообщение в лог. */ function addToLog(message, type = 'info') { // Используем строковый тип по умолчанию const logListElement = uiElements.log.list; if (!logListElement) { console.warn("addToLog: Элемент списка логов не найден."); return; } const li = document.createElement('li'); li.textContent = message; // Используем типы логов из GAME_CONFIG, если он определен // Иначе, используем переданный строковый тип как есть const logTypeClass = window.GAME_CONFIG && window.GAME_CONFIG[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${window.GAME_CONFIG[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`; li.className = logTypeClass; logListElement.appendChild(li); // Плавная прокрутка к последнему сообщению requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; }); } /** Обновляет бары, статус, имя и аватар бойца. */ function updateFighterUI(fighterRole, // 'player' (Елена) или 'opponent' (Балард) - технические роли fighterActualId, // 'player' или 'opponent' - кто из gameState соответствует этой панели fighterState, // gameState[fighterActualId] fighterBaseStatsRef, // gameData[fighterActualId+'BaseStats'] isControlledByThisClient) { // true, если эта панель соответствует игроку за этим клиентом const elements = uiElements[fighterRole]; // uiElements.player или uiElements.opponent if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStatsRef) { console.warn(`updateFighterUI: Отсутствуют элементы/состояние/базовые статы для роли ${fighterRole} (id: ${fighterActualId}).`); return; } // Обновление имени и иконки if (elements.name) { let nameHtml = ` ${fighterBaseStatsRef.name}`; if (isControlledByThisClient) { nameHtml += " (Вы)"; } elements.name.innerHTML = nameHtml; elements.name.style.color = fighterBaseStatsRef.id === 'player' ? 'var(--accent-player)' : 'var(--accent-opponent)'; } // Обновление аватара (если путь к аватару есть в baseStats или приходит от сервера) // if (elements.avatar && fighterBaseStatsRef.avatarPath) { // elements.avatar.src = fighterBaseStatsRef.avatarPath; // } const maxHp = Math.max(1, fighterBaseStatsRef.maxHp); const maxRes = Math.max(1, fighterBaseStatsRef.maxResource); const currentHp = Math.max(0, fighterState.currentHp); const currentRes = Math.max(0, fighterState.currentResource); const hpPercent = (currentHp / maxHp) * 100; const resourcePercent = (currentRes / maxRes) * 100; elements.hpFill.style.width = `${hpPercent}%`; elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStatsRef.maxHp}`; elements.resourceFill.style.width = `${resourcePercent}%`; elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStatsRef.maxResource}`; // Динамическое изменение цвета и иконки для ресурса Баларда (если он не мана) const resourceBarContainer = elements.resourceFill.closest('.stat-bar-container'); if (resourceBarContainer) { const iconElement = resourceBarContainer.querySelector('.bar-icon i'); if (fighterActualId === (window.GAME_CONFIG?.OPPONENT_ID || 'opponent') && fighterBaseStatsRef.resourceName !== "Мана") { resourceBarContainer.classList.remove('mana'); resourceBarContainer.classList.add('stamina'); // Предполагаем, что это выносливость/ярость if(iconElement) iconElement.className = 'fas fa-fire-alt'; // Иконка ярости } else { // Для Елены или если у Баларда мана resourceBarContainer.classList.remove('stamina'); resourceBarContainer.classList.add('mana'); if(iconElement) iconElement.className = 'fas fa-flask'; // Иконка маны } } const statusText = fighterState.isBlocking ? (window.GAME_CONFIG?.STATUS_BLOCKING || 'Защищается') : (window.GAME_CONFIG?.STATUS_READY || 'Готов'); elements.status.textContent = statusText; elements.status.classList.toggle(window.GAME_CONFIG?.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking); // Обновление glow-эффекта панели if (elements.panel) { const accentColor = fighterActualId === (window.GAME_CONFIG?.PLAYER_ID || 'player') ? 'var(--accent-player)' : 'var(--accent-opponent)'; const glowColor = fighterActualId === (window.GAME_CONFIG?.PLAYER_ID || 'player') ? 'var(--panel-glow-player)' : 'var(--panel-glow-opponent)'; elements.panel.style.borderColor = accentColor; elements.panel.style.boxShadow = `0 0 15px ${glowColor}, inset 0 0 10px rgba(0, 0, 0, 0.3)`; } } function classifyAndGenerateEffectsHTMLArrays(effectsArray, fighterRole) { const buffsHTMLStrings = []; const debuffsHTMLStrings = []; const config = window.GAME_CONFIG || {}; // Фоллбэк на пустой объект, если GAME_CONFIG не загружен if (!effectsArray || effectsArray.length === 0) { return { buffsHTMLStrings: ['Нет'], debuffsHTMLStrings: ['Нет'] }; } effectsArray.forEach(eff => { let isDebuff = false; 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.type === config.ACTION_TYPE_DISABLE || eff.type === config.ACTION_TYPE_DEBUFF || eff.id === 'fullSilenceByElena' || // Специфичный эффект Елены eff.id.startsWith('playerSilencedOn_') // Эффект безмолвия от Баларда на конкретную абилку ) { isDebuff = true; } if (isDebuff) { if (eff.id === 'fullSilenceByElena' || eff.id.startsWith('playerSilencedOn_') || eff.type === config.ACTION_TYPE_DISABLE) { effectClasses += ' effect-stun'; // Или ваш класс для дизейблов } else { effectClasses += ' effect-debuff'; } debuffsHTMLStrings.push(`${displayText}`); } else { if (eff.grantsBlock) { effectClasses += ' effect-block'; } else { // Все остальное считаем баффом effectClasses += ' effect-buff'; } buffsHTMLStrings.push(`${displayText}`); } }); return { buffsHTMLStrings: buffsHTMLStrings.length > 0 ? buffsHTMLStrings : ['Нет'], debuffsHTMLStrings: debuffsHTMLStrings.length > 0 ? debuffsHTMLStrings : ['Нет'] }; } function updateEffectsUI(currentGameState, myPlayerId) { if (!currentGameState || !window.gameData) return; // gameData тоже должен быть доступен const playerRoleForUI = myPlayerId === currentGameState.player.id ? 'player' : 'opponent'; const opponentRoleForUI = myPlayerId === currentGameState.player.id ? 'opponent' : 'player'; // Обновление для панели, соответствующей myPlayerId const myPanelElements = uiElements[playerRoleForUI]; // Это будет uiElements.player или uiElements.opponent const myState = currentGameState[myPlayerId]; // Это будет gameState.player или gameState.opponent if (myPanelElements && myState) { const myClassifiedEffects = classifyAndGenerateEffectsHTMLArrays(myState.activeEffects, playerRoleForUI); if (myPanelElements.buffsList) myPanelElements.buffsList.innerHTML = myClassifiedEffects.buffsHTMLStrings.join(' '); if (myPanelElements.debuffsList) myPanelElements.debuffsList.innerHTML = myClassifiedEffects.debuffsHTMLStrings.join(' '); } // Обновление для панели противника const opponentActualId = myPlayerId === currentGameState.player.id ? currentGameState.opponent.id : currentGameState.player.id; const opponentPanelElements = uiElements[opponentRoleForUI]; const opponentState = currentGameState[opponentActualId]; if (opponentPanelElements && opponentState) { const opponentClassifiedEffects = classifyAndGenerateEffectsHTMLArrays(opponentState.activeEffects, opponentRoleForUI); if (opponentPanelElements.buffsList) opponentPanelElements.buffsList.innerHTML = opponentClassifiedEffects.buffsHTMLStrings.join(' '); if (opponentPanelElements.debuffsList) opponentPanelElements.debuffsList.innerHTML = opponentClassifiedEffects.debuffsHTMLStrings.join(' '); } } /** Главная функция обновления всего UI. */ function updateUI() { // Используем глобальные gameState, gameData, GAME_CONFIG, установленные из client.js const currentGameState = window.gameState; const gameDataGlobal = window.gameData; const configGlobal = window.GAME_CONFIG; const myPlayerId = window.myPlayerId; // myPlayerId также должен быть установлен глобально или передан if (!currentGameState || !gameDataGlobal || !configGlobal || !myPlayerId) { console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myPlayerId."); return; } if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) { console.warn("updateUI: Некоторые базовые uiElements не найдены."); return; } // Определяем, какая панель (player/opponent в uiElements) соответствует какому игроку (player/opponent в gameState) // и кто из них управляется этим клиентом. const playerPanelRepresents = currentGameState.player.id; // Кого отображает панель "player" const opponentPanelRepresents = currentGameState.opponent.id; // Кого отображает панель "opponent" updateFighterUI('player', playerPanelRepresents, currentGameState[playerPanelRepresents], gameDataGlobal[playerPanelRepresents === 'player' ? 'playerBaseStats' : 'opponentBaseStats'], playerPanelRepresents === myPlayerId); updateFighterUI('opponent', opponentPanelRepresents, currentGameState[opponentPanelRepresents], gameDataGlobal[opponentPanelRepresents === 'player' ? 'playerBaseStats' : 'opponentBaseStats'], opponentPanelRepresents === myPlayerId); updateEffectsUI(currentGameState, myPlayerId); // Обновление заголовка игры (Елена vs Балард) if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) { uiElements.gameHeaderTitle.innerHTML = `${gameDataGlobal.playerBaseStats.name} ${gameDataGlobal.opponentBaseStats.name}`; } if (uiElements.controls.turnIndicator) { const currentTurnActorId = currentGameState.isPlayerTurn ? currentGameState.player.id : currentGameState.opponent.id; const currentTurnName = currentGameState[currentTurnActorId].name; uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`; uiElements.controls.turnIndicator.style.color = currentTurnActorId === configGlobal.PLAYER_ID ? 'var(--accent-player)' : 'var(--accent-opponent)'; } const canThisClientAct = (currentGameState.isPlayerTurn && myPlayerId === currentGameState.player.id) || (!currentGameState.isPlayerTurn && myPlayerId === currentGameState.opponent.id); const isGameActive = !currentGameState.isGameOver; // Обновление кнопки атаки if (uiElements.controls.buttonAttack) { uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive); // Подсветка атаки от "Силы Природы" (только если myPlayerId - это Елена) if (myPlayerId === configGlobal.PLAYER_ID) { const isNatureBuffReady = currentGameState.player.activeEffects.some(eff => eff.id === configGlobal.ABILITY_ID_NATURE_STRENGTH && !eff.justCast); uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isNatureBuffReady && canThisClientAct && isGameActive); } else { uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed'); } } // Кнопка блока всегда отключена if (uiElements.controls.buttonBlock) { uiElements.controls.buttonBlock.disabled = true; } // Обновление кнопок способностей для текущего игрока (myPlayerId) const actingPlayerState = currentGameState[myPlayerId]; const actingPlayerAbilities = myPlayerId === configGlobal.PLAYER_ID ? gameDataGlobal.playerAbilities : gameDataGlobal.opponentAbilities; uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => { if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities) { if(button instanceof HTMLButtonElement) button.disabled = true; return; } const abilityId = button.dataset.abilityId; const ability = actingPlayerAbilities.find(ab => ab.id === abilityId); if (!ability) { button.disabled = true; return; } const hasEnoughResource = actingPlayerState.currentResource >= ability.cost; const isBuffAlreadyActive = ability.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects.some(eff => eff.id === abilityId); const silencedInfo = actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId); const isSilencedByOpponent = !!silencedInfo; const cooldownTurnsLeft = actingPlayerState.abilityCooldowns?.[ability.id] || 0; const isOnCooldown = cooldownTurnsLeft > 0; let isDisabledByDebuffOnTarget = false; // Для специфичных дебаффов типа Печати Слабости if (ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS && myPlayerId === configGlobal.PLAYER_ID) { // Только Елена может накладывать const opponentActualId = myPlayerId === currentGameState.player.id ? currentGameState.opponent.id : currentGameState.player.id; const effectIdForDebuff = 'effect_' + ability.id; isDisabledByDebuffOnTarget = currentGameState[opponentActualId].activeEffects.some(e => e.id === effectIdForDebuff); } let finalDisabledState = !(canThisClientAct && isGameActive) || !hasEnoughResource || isBuffAlreadyActive || isSilencedByOpponent || isOnCooldown || isDisabledByDebuffOnTarget; button.disabled = finalDisabledState; // Стилизация кнопки 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 = `КД: ${cooldownTurnsLeft}`; cooldownDisplay.style.display = 'block'; } } else { if (cooldownDisplay) cooldownDisplay.style.display = 'none'; // Применяем другие классы только если не на КД button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE || 'not-enough-resource', canThisClientAct && isGameActive && !hasEnoughResource && !isBuffAlreadyActive && !isSilencedByOpponent && !isDisabledByDebuffOnTarget); button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE || 'buff-is-active', canThisClientAct && isGameActive && isBuffAlreadyActive && !isSilencedByOpponent); // Для баффов на себя button.classList.toggle(configGlobal.CSS_CLASS_ABILITY_SILENCED || 'is-silenced', isSilencedByOpponent); // Если Печать Слабости уже на цели, можно добавить особый класс или просто дизейблить if (isDisabledByDebuffOnTarget) { // button.classList.add('debuff-already-active'); // Пример } } // Обновление Tooltip const resourceName = actingPlayerState.resourceName; let titleText = `${ability.name} (${ability.cost} ${resourceName}) - ${ability.description}`; if (typeof ability.descriptionFunction === 'function') { const configForDesc = window.clientSideConfig?.GAME_CONFIG_FOR_ABILITIES || configGlobal; const targetStatsForDesc = (myPlayerId === configGlobal.PLAYER_ID) ? (window.gameData?.opponentBaseStats) : (window.gameData?.playerBaseStats); titleText = `${ability.name} (${ability.cost} ${resourceName}) - ${ability.descriptionFunction(configForDesc, targetStatsForDesc)}`; } let abilityBaseCooldown = ability.cooldown; if (myPlayerId === configGlobal.OPPONENT_ID) { // Если Балард-игрок if (ability.internalCooldownFromConfig && configGlobal) { abilityBaseCooldown = configGlobal[ability.internalCooldownFromConfig]; } else if (ability.internalCooldownValue) { abilityBaseCooldown = ability.internalCooldownValue; } } if (abilityBaseCooldown) titleText += ` (КД: ${abilityBaseCooldown} х.)`; if (isOnCooldown) { titleText = `${ability.name} - На перезарядке! Осталось: ${cooldownTurnsLeft} х.`; } else if (isSilencedByOpponent) { titleText = `Заглушено! Осталось: ${silencedInfo.turnsLeft} х.`; } else if (isBuffAlreadyActive) { const activeEffect = actingPlayerState.activeEffects.find(eff => eff.id === abilityId); titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`; } else if (isDisabledByDebuffOnTarget) { const opponentActualId = myPlayerId === currentGameState.player.id ? currentGameState.opponent.id : currentGameState.player.id; const activeDebuff = currentGameState[opponentActualId].activeEffects.find(e => e.id === 'effect_' + ability.id); titleText = `Эффект "${ability.name}" уже наложен на ${currentGameState[opponentActualId].name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`; } button.setAttribute('title', titleText); }); } /** Показывает экран конца игры с задержкой. */ function showGameOver(playerWon, reason = "") { const config = window.GAME_CONFIG || {}; const gameDataGlobal = window.gameData || {}; const gameOverScreenElement = uiElements.gameOver.screen; if (!gameOverScreenElement) return; const resultMsgElement = uiElements.gameOver.message; const opponentPanelElement = uiElements.opponent.panel; // Панель Баларда в UI if (resultMsgElement) { let winText = `Победа! ${gameDataGlobal.playerBaseStats ? gameDataGlobal.playerBaseStats.name : 'Игрок'} празднует!`; let loseText = `Поражение! ${gameDataGlobal.opponentBaseStats ? gameDataGlobal.opponentBaseStats.name : 'Противник'} оказался сильнее!`; if (reason === 'opponent_disconnected') { if (playerWon) { // Если "мы" победили из-за дисконнекта оппонента winText = `Противник покинул игру. Победа присуждается вам!`; } else { // Если "мы" были оппонентом, который остался loseText = `Игрок покинул игру. Техническая победа.`; } } if (playerWon) { resultMsgElement.textContent = winText; resultMsgElement.style.color = 'var(--heal-color)'; } else { resultMsgElement.textContent = loseText; resultMsgElement.style.color = 'var(--damage-color)'; } } // Анимация растворения панели Баларда, если победила Елена (и это не дисконнект) // И если панель оппонента действительно отображает Баларда const opponentPanelRepresents = window.gameState?.opponent.id; // Кто сейчас на панели оппонента if (playerWon && reason !== 'opponent_disconnected' && opponentPanelElement && opponentPanelRepresents === config.OPPONENT_ID) { // addToLog вызывается из client.js при получении gameOver от сервера opponentPanelElement.classList.add('dissolving'); } else if (opponentPanelElement) { // Убираем класс, если он был, на случай рестарта opponentPanelElement.classList.remove('dissolving'); } setTimeout(() => { gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden'); requestAnimationFrame(() => { // Для плавности анимации появления gameOverScreenElement.style.opacity = '0'; // Сначала делаем невидимым setTimeout(() => { // Даем браузеру время на рендер невидимого состояния gameOverScreenElement.style.opacity = '1'; // Анимируем появление if (uiElements.gameOver.modalContent) { uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)'; uiElements.gameOver.modalContent.style.opacity = '1'; } }, config.MODAL_TRANSITION_DELAY || 10); }); }, config.DELAY_BEFORE_VICTORY_MODAL || 1500); } // --- Экспорт --- // Функции теперь доступны через window.gameUI window.gameUI = { uiElements, addToLog, updateUI, showGameOver // classifyAndGenerateEffectsHTMLArrays, // Можно сделать доступным, если нужно извне // updateFighterUI, // updateEffectsUI }; })(); // console.log("ui.js loaded and initialized.", window.gameUI);